Shell redirection example

A friend emailed me to ask:

[/tmp] $ ls > foo > bar
[/tmp] $ ls -lh foo bar
-rw-r--r-- 1 dsh wheel 7.1K Sep 26 16:01 bar
-rw-r--r-- 1 dsh wheel 0B Sep 26 16:01 foo
[/tmp] $

What is happening here?

It’s surprising at first glance, but it’s simpler than it looks. Bash supports as many redirections as you want of whichever file descriptors you want. They’re processed from left to right. That’s why these two behave differently:

$ ls > /dev/null 2>&1    # redirects stdout and stderr to /dev/null
$ ls 2>&1 > /dev/null    # redirects stderr to your terminal and stdout to /dev/null

By far, the simplest way to implement this is to do it in the child process after fork, but before exec. So the first example does:

open("/dev/null", O_WRONLY | O_CREAT | O_TRUNC);
dup2(1, 2);

while the second does:

dup2(1, 2);
open("/dev/null", O_WRONLY | O_CREAT | O_TRUNC);

So the original example probably does:

open("foo", O_WRONLY | O_CREAT | O_TRUNC);
open("bar", O_WRONLY | O_CREAT | O_TRUNC);

which causes “foo” to be created as an empty file, and “bar” to be created with the output of “ls”.

Follow-up question: why doesn’t bash either consider this invocation an error or skip the first redirection, knowing the second will logically clobber it? The actual behavior is implied by the documented behavior that the redirections are processed from left to right. (Error-checking or more efficient behavior would also be possible, but it’s certainly not implied by the way it’s documented.) I’d guess it works this way because it’s very simple to implement and reason about (if surprising), since it’s just going left-to-right executing one of a very small number of fd-modifying syscalls. Depending on what kinds of expansion are processed before this, doing something smarter might be pretty difficult.

I don’t mean to imply any of that is not surprising. I use the “> foo 2>&1” construct all the time and have to evaluate the state machine each time to remember which order it goes in. It’s especially confusing because if you want to redirect both over a pipe, you use “2>&1 |” (i.e., the opposite order).