Search code examples
bashshellpipestdoutpipeline

Why does 2>&1 need to come before a | (pipe) but after a "> myfile" (redirect to file)?


When combining stderr with stdout, why does 2>&1 need to come before a | (pipe) but after a > myfile (redirect to file)?

To redirect stderr to stdout for file output:

  echo > myfile 2>&1

To redirect stderr to stdout for a pipe:

  echo 2>&1 | less



My assumption was that I could just do:

  echo | less 2>&1 

and it would work, but it doesn't. Why not?


Solution

  • A pipeline is a |-delimited list of commands. Any redirections you specify apply to the constituent commands (simple or compound), but not to the pipeline as a whole. Each pipe chains one command's stdout to the stdin of the next by implicitly applying a redirect to each subshell before any redirects associated with a command are evaluated.

    cmd 2>&1 | less
    

    First stdout of the first subshell is redirected to the pipe from which less is reading. Next, the 2>&1 redirect is applied to the first command. Redirecting stderr to stdout works because stdout is already pointing at the pipe.

    cmd | less 2>&1
    

    Here, the redirect applies to less. Less's stdout and stderr both presumably started out pointed at the terminal, so 2>&1 in this case has no effect.

    If you want a redirect to apply to an entire pipeline, to group multiple commands as part of a pipeline, or to nest pipelines, then use a command group (or any other compound command):

    { { cmd1 >&3; cmd2; } 2>&1 | cmd3; } 3>&2
    

    Might be a typical example. The end result is: cmd1 and cmd2's stderr -> cmd3; cmd2's stdout -> cmd3; and cmd1 and cmd3's stderr, and cmd3's stdout -> the terminal.

    If you use the Bash-specific |& pipe, things get stranger, because each of the pipeline's stdout redirects still occur first, but the stderr redirect actually comes last. So for example:

    f() { echo out; echo err >&2; }; f >/dev/null |& cat
    

    Now, counterintuitively, all output is hidden. First stdout of f goes to the pipe, next stdout of f is redirected to /dev/null, and finally, stderr is redirected to stdout (/dev/null still).

    I recommend never using |& in Bash -- it's used here for demonstration.