Catch exit codes in bash - pipefail vs PIPESTATUS

Introduction

The other day I caught a bug in one of my scripts, where I wanted to both act on the exit code of a command at the same time as I sent the output from it to a log file. The construct:

Featured image

myCommand | tee -a file.log
rc=$?

Simple enough. I expected that I would be able to catch the exit code of “myCommand” and act on the $rc variable at a later stage in my script. I was wrong. As long as I can write to file.log, my $rc variable will always be 0.

At first it annoyed me a bit, as I had completely forgotten about this. Well, this is the expected behaviour, and the way it works is quite useful in other cases. For example, consider the following example:

$ if cat /etc/passwd | grep --silent "^root" ; then echo "Root is in /etc/passwd"; fi
Root is in /etc/passwd

Here we really want the exit code of the second statement, the “grep” statement after the pipe, to be evaluated. So in most cases the default behavior works to our advantage. But how do we change this to catch the command feeding the pipe? The answer is:

  • set -pipefail
  • the builtin array PIPESTATUS

When setting “pipefail” in your script, the return code will be the first non zero exit code when working yourself backwards in the statement. Example:

$ $(exit 2) | $(exit 1) | $(exit 0)
$ echo $?
0
$ set -o pipefail
$ $(exit 2) | $(exit 1) | $(exit 0)
$ echo $?
1
$ $(exit 1) | $(exit 2) | $(exit 0)
$ echo $?
2
$ $(exit 1) | $(exit 0) | $(exit 0)
$ echo $?
1

While “pipefail” might be tempting to use, I would rather avoid it since most people who will read your scripts and chase bugs will not be used to it.

Enter: PIPESTATUS

PIPESTATUS is a builtin array in bash (and zsh) that catches all exit codes of your piped commands:

vagrant@dash$ $(exit 1) | $(exit 2) | $(exit 0)
vagrant@dash$ echo ${PIPESTATUS[@]}
1 2 0

This is very useful, but the array is reset as soon as you run a command (i.e after you display it with “echo”). To copy the whole array, you will have to do the following:

$ $(exit 1) | $(exit 2) | $(exit 0)
$ echo ${PIPESTATUS[@]}
1 2 0
$ echo ${PIPESTATUS[@]}
0

$ $(exit 1) | $(exit 2) | $(exit 0)
$ allRc=("${PIPESTATUS[@]}")
$ echo ${allRc[@]}
1 2 0
$ echo ${allRc[0]}
1
$ echo ${allRc[1]}
2
$ echo ${allRc[2]}
0

And finally, I updated my script to become:

myCommand | tee -a file.log
rc=${PIPESTATUS[0]}