Search code examples
shellwhile-loopshposixheredoc

Why are heredoc redirections not allowed before a while loop


Background

The POSIX Shell Command Language allows redirections to follow compound commands. The standard says (emphasis mine)

2.9.4 Compound Commands

The shell has several programming constructs that are "compound commands", which provide control flow for commands. Each of these compound commands has a reserved word or control operator at the beginning, and a corresponding terminator reserved word or operator at the end. In addition, each can be followed by redirections on the same line as the terminator. Each redirection shall apply to all the commands within the compound command that do not explicitly override that redirection.

For aesthetic reasons I want to put a heredoc before my while loop as in

<<'RECORDS' while
foo:bar
baz:quux
...
RECORDS
   IFS=: read -r A B
do
  # do something with A and B
done

because it makes the code easier to follow. However, it doesn't work in the shells I tried it (bash and dash). I get errors saying that the "while" command was not found and I assume that means that after the leading heredoc a simple command is expected and not a compound command.

I cannot move the heredoc to after read because then it reads the first line from a new heredoc on every iteration. I know that I can fix this by moving the heredoc to after done. I could also open a fd to a heredoc with exec before the loop and add a redirection to read.

My question

What's the reason redirections cannot occur before a compound command? Is there a shell that supports it since it's not explicitly prohibited by POSIX?


Solution

  • The Z shell (zsh) will accept this non-standard syntax. POSIX standardizes existing practice, and in the case of the shell, the reference implementation was the Korn shell (ksh88, or just ksh). Since that implementation supported only redirections following the compound statement that was what was standardized.

    There are several ways you can write your loop in a more portable (and much easier to read) form. For example:

    while
      IFS=: read -r A B
    do
        echo $A $B
    done <<'RECORDS'
    foo:bar
    baz:quux
    RECORDS
    

    is the commonest way I would do something like this. Or you could wrap the loop up in a function and redirect the input to the function:

    loop()
    {
        while
          IFS=: read -r A B
        do
            echo $A $B
        done
    }
    
    <<'RECORDS' loop
    foo:bar
    baz:quux
    RECORDS
    

    This allows you to mix up the call to the function in amid the data as you originally wanted (I still don't really understand why you think this is clearer:-) ) Both these techniques work with bash, dash, ksh, ksh93, yash and zsh.