Bash Syntax – Why Input Redirection Order Can’t Be Reversed in While Loops

bashio-redirectionsyntax

In Bash you can move input redirection operators to the front of a command:

cat <<< "hello"
# equivalent to
<<< "hello" cat

Why are you unable to do the same for while loops?

while read -r line; do echo "$line"; done <<< "hello"
# hello

<<< "hello" while read -r line; do echo "$line"; done
# -bash: syntax error near unexpected token `do'

I find it a bit confusing since you can pipe into a while loop. Am I doing something wrong or was it just a design decision?

Best Answer

It's just a consequence of how the grammar is defined. From the POSIX Shell Grammar specification:

command          : simple_command
                 | compound_command
                 | compound_command redirect_list
                 | function_definition
                 ;

And:

simple_command   : cmd_prefix cmd_word cmd_suffix
                 | cmd_prefix cmd_word
                 | cmd_prefix
                 | cmd_name cmd_suffix
                 | cmd_name
                 ;
[...]
cmd_prefix       :            io_redirect
                 | cmd_prefix io_redirect
                 |            ASSIGNMENT_WORD
                 | cmd_prefix ASSIGNMENT_WORD
                 ;
cmd_suffix       :            io_redirect
                 | cmd_suffix io_redirect
                 |            WORD
                 | cmd_suffix WORD
                 ;

As you can see, with compound commands, redirection is only allowed after, but with simple commands, it is allowed before as well. So, when the shell sees <redirection> foo, foo is treated as a simple command, not a compound command, and while is no longer treated as a keyword:

$ < foo while
bash: while: command not found

Hence, the do is unexpected, since it's only allowed after certain keywords.

So this applies not just to while loops, but most of the ways of setting up compound commands using reserved words:

$ < foo {
bash: {: command not found
$ < foo if
bash: if: command not found
$ < foo for
bash: for: command not found
Related Question