Get exit status of process that's piped to another
bash
and zsh
have an array variable that holds the exit status of each element (command) of the last pipeline executed by the shell.
If you are using bash
, the array is called PIPESTATUS
(case matters!) and the array indicies start at zero:
$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0
If you are using zsh
, the array is called pipestatus
(case matters!) and the array indices start at one:
$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0
To combine them within a function in a manner that doesn't lose the values:
$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0
Run the above in bash
or zsh
and you'll get the same results; only one of retval_bash
and retval_zsh
will be set. The other will be blank. This would allow a function to end with return $retval_bash $retval_zsh
(note the lack of quotes!).
There are 3 common ways of doing this:
Pipefail
The first way is to set the pipefail
option (ksh
, zsh
or bash
). This is the simplest and what it does is basically set the exit status $?
to the exit code of the last program to exit non-zero (or zero if all exited successfully).
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
$PIPESTATUS
Bash also has an array variable called $PIPESTATUS
($pipestatus
in zsh
) which contains the exit status of all the programs in the last pipeline.
$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1
You can use the 3rd command example to get the specific value in the pipeline that you need.
Separate executions
This is the most unwieldy of the solutions. Run each command separately and capture the status
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
This solution works without using bash specific features or temporary files. Bonus: in the end the exit status is actually an exit status and not some string in a file.
Situation:
someprog | filter
you want the exit status from someprog
and the output from filter
.
Here is my solution:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
the result of this construct is stdout from filter
as stdout of the construct and exit status from someprog
as exit status of the construct.
this construct also works with simple command grouping {...}
instead of subshells (...)
. subshells have some implications, among others a performance cost, which we do not need here. read the fine bash manual for more details: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
Unfortunately the bash grammar requires spaces and semicolons for the curly braces so that the construct becomes much more spacious.
For the rest of this text I will use the subshell variant.
Example someprog
and filter
:
someprog() {
echo "line1"
echo "line2"
echo "line3"
return 42
}
filter() {
while read line; do
echo "filtered $line"
done
}
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
Example output:
filtered line1
filtered line2
filtered line3
42
Note: the child process inherits the open file descriptors from the parent. That means someprog
will inherit open file descriptor 3 and 4. If someprog
writes to file descriptor 3 then that will become the exit status. The real exit status will be ignored because read
only reads once.
If you worry that your someprog
might write to file descriptor 3 or 4 then it is best to close the file descriptors before calling someprog
.
(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
The exec 3>&- 4>&-
before someprog
closes the file descriptor before executing someprog
so for someprog
those file descriptors simply do not exist.
It can also be written like this: someprog 3>&- 4>&-
Step by step explanation of the construct:
( ( ( ( someprog; #part6
echo $? >&3 #part5
) | filter >&4 #part4
) 3>&1 #part3
) | (read xs; exit $xs) #part2
) 4>&1 #part1
From bottom up:
- A subshell is created with file descriptor 4 redirected to stdout. This means that whatever is printed to file descriptor 4 in the subshell will end up as the stdout of the entire construct.
- A pipe is created and the commands on the left (
#part3
) and right (#part2
) are executed.exit $xs
is also the last command of the pipe and that means the string from stdin will be the exit status of the entire construct. - A subshell is created with file descriptor 3 redirected to stdout. This means that whatever is printed to file descriptor 3 in this subshell will end up in
#part2
and in turn will be the exit status of the entire construct. - A pipe is created and the commands on the left (
#part5
and#part6
) and right (filter >&4
) are executed. The output offilter
is redirected to file descriptor 4. In#part1
the file descriptor 4 was redirected to stdout. This means that the output offilter
is the stdout of the entire construct. - Exit status from
#part6
is printed to file descriptor 3. In#part3
file descriptor 3 was redirected to#part2
. This means that the exit status from#part6
will be the final exit status for the entire construct. someprog
is executed. The exit status is taken in#part5
. The stdout is taken by the pipe in#part4
and forwarded tofilter
. The output fromfilter
will in turn reach stdout as explained in#part4