This is what I consider to be the best pure-Bourne-shell solution to use as the base upon which you could build your "eet":
# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; echo $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.
I think this is best explained from the inside out – command1
will execute and print its regular output on stdout (file descriptor 1), then once it's done, echo
will execute and print command1
's exit code on its stdout, but that stdout is redirected to file descriptor three.
While command1
is running, its stdout is being piped to command2 (echo
's output never makes it to command2 because we send it to file descriptor 3 instead of 1, which is what the pipe reads). Then we redirect command2
's output to file descriptor 4, so that it also stays out of file descriptor one – because we want file descriptor one clear for when we bring the echo
output on file descriptor three back down into file descriptor one so that the command substitution (the backticks) can capture it.
The final bit of magic is that first exec 4>&1
we did as a separate command – it opens file descriptor four as a copy of the external shell's stdout. Command substitution will capture whatever is written on standard out from the perspective of the commands inside it – but, since command2
's output is going to file descriptor four as far as the command substitution is concerned, the command substitution doesn't capture it – however, once it gets "out" of the command substitution, it is effectively still going to the script's overall file descriptor one.
(The exec 4>&1
has to be a separate command to work with many common shells. In some shells it works if you just put it on the same line as the variable assignment, after the closing backtick of the substitution.)
(I use compound commands ({ ... }
) in my example, but subshells (( ... )
) would also work. The subshell will just cause a redundant forking and awaiting of a child process, since each side of a pipe and the inside of a command substitution already normally implies a fork and await of a child process, and I don't know of any shell being coded to recognize that it can skip one of those forks because it's already done or is about to do the other.)
You can look at it in a less technical and more playful way, as if the outputs of the commands are leapfrogging each other: command1
pipes to command2
, then the echo
's output jumps over command2
so that command2
doesn't catch it, and then command2
's output jumps over and out of the command substitution just as echo
lands just in time to get captured by the substitution so that it ends up in the variable, and command2
's output goes on its way to the standard output, just as in a normal pipe.
Also, as I understand it, at the end of this command, $?
will still contain the return code of the second command in the pipe, because variable assignments, command substitutions, and compound commands are all effectively transparent to the return code of the command inside them, so the return status of command2
should get propagated out.
A caveat is that it is possible that command1
will at some point end up using file descriptors three or four, or that command2
or any of the later commands will use file descriptor four, so to be more hygienic, we would do:
exec 4>&1
exitstatus=`{ { command1 3>&-; echo $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
Commands inherit file descriptors from the process that launches them, so the entire second line will inherit file descriptor four, and the compound command followed by 3>&1
will inherit the file descriptor three. So the 4>&-
makes sure that the inner compound command will not inherit file descriptor four, and the 3>&-
makes sure that command1
will not inherit file descriptor three, so command1 gets a 'cleaner', more standard environment. You could also move the inner 4>&-
next to the 3>&-
, but I figure why not just limit its scope as much as possible.
Almost no programs uses pre-opened file descriptor three and four directly, so you almost never have to worry about it, but the latter is probably best to keep in mind and use for general-purpose cases.