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:

  1. 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.
  2. 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.
  3. 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.
  4. A pipe is created and the commands on the left (#part5 and #part6) and right (filter >&4) are executed. The output of filter is redirected to file descriptor 4. In #part1 the file descriptor 4 was redirected to stdout. This means that the output of filter is the stdout of the entire construct.
  5. 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.
  6. someprog is executed. The exit status is taken in #part5. The stdout is taken by the pipe in #part4 and forwarded to filter. The output from filter will in turn reach stdout as explained in #part4

Tags:

Shell

Exit

Pipe