It helps a bit if you think the file descriptors as variables that accept a file as a value (or call it an i/o stream) and the order they appear is the order of their evaluation.
What happens in the above example is:
1) The script starts (as per default and unless otherwise inherited) with the following
fd/0 = stdin # that's the keyboard
fd/1 = stdout # that's the screen
fd/2 = stderr # the screen again, but different stream
2) The exec
command translates to declaring a new variable and assigning a value
fd/3 = fd/1 # same as stdout
So now, two file descriptors have the value stdout, i.e. both can be used to print to the screen.
3) before ls
is executed and inherits all open file descriptors, the following setup happens
ls.fd/1 = grep.fd/0 # pipe gets precedence, ls.fd/1 writes to grep.stdin
ls.fd/2 = ls.fd/1 # ls.fd/2 writes to grep.stdin
ls.fd/1 = ls.fd/3 # ls.fd/1 writes to stdout
ls.fd/3 = closed # fd/3 will not be inherited by `ls`
fd/3 has served the purpose of keeping the stdout value long enough to return it to fd/1. So now everything that ls
sends to fd/1 goes to stdout and not grep
's stdin.
The order is important, e.g. if we'd run ls -l >&3 2>&1 3>&-
, ls.fd/2 would write to stdout instead of grep
's stdin.
4) fd/3 for grep
is closed and not inherited. It would be unused anyway. grep
can only filter error messages from ls
The example provided in ABSG is probably not the most helpful and the comment "Close fd 3 for 'grep' (but not 'ls')" is a bit misleading. You can interpret it as: "for the ls, pass the value of ls.fd/3 to ls.fd/1 before unsetting so it won't get closed".
You could do (POSIXly):
if { cmd 2>&1 >&3 3>&- | grep '^' >&2; } 3>&1; then
echo there was some output on stderr
fi
Or to preserve the original exit status if it was non-zero:
fail_if_stderr() (
rc=$({
("$@" 2>&1 >&3 3>&- 4>&-; echo "$?" >&4) |
grep '^' >&2 3>&- 4>&-
} 4>&1)
err=$?
[ "$rc" -eq 0 ] || exit "$rc"
[ "$err" -ne 0 ] || exit 125
) 3>&1
Using exit code 125
for the cases where the command returns with a 0 exit status but produced some error output.
To be used as:
fail_if_stderr cmd its args || echo "Failed with $?"
Best Answer
Obviously closing stdout does not fail, on the contrary, it succeeds because writing on it fails, as can be seen from the error message. Edit: to clarify my answer, what happens is that you first tell the shell to close the file descriptor, then the
ls
program tries to write to it. This is where the error message comes from.