1. Overview

In Bash, there might be cases where we pipe commands and want to check the exit status of one of the commands in the pipeline. An example might be a long-running process whose output we want to check while it’s running:

$ long_running_script.sh 2>&1 | tee output_of_script

In this example, we are probably interested in the exit status of the script, long_running_script.sh. But running the echo $? command immediately after the execution of the piped commands gives the exit status of the tee command.

In this tutorial, we’ll go over getting the exit status of piped commands.

2. The Analysis of the Problem

First, let’s begin with the analysis of the exit status of piped commands. We have the following script, hello_world.sh:

#!/bin/bash
echo "Hello World"
exit $1

This Bash script just prints Hello World and exits with the exit status supplied as a parameter to the script:

$ hello_world.sh 0
Hello World
$ echo $?
0
$ hello_world.sh 5
Hello World
$ echo $?
5

Now, let’s search the Hello World in the output of this script by using the grep command and then check the exit status of the pipeline:

$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
0

As we see, although the exit status of the hello_world.sh script is 5, the exit status of the piped commands is 0. In fact, the exit status of the piped commands is the exit status of the last command in the piped commands. In our example, it is the exit status of the grep “Hello World” command. Let’s prove it by searching a non-existing word in the output of the script, for example, Hello Universe:

$ hello_world.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo $?
1

Now, the exit status of the piped commands is 1 since the exit status of the last command in the pipeline, grep “Hello Universe”, is 1.

3. The PIPESTATUS Variable

The PIPESTATUS environment variable in Bash comes to our rescue for getting the exit status of each command in a pipeline. $PIPESTATUS is an array. It stores the exit status of each command in the pipeline:

$ hello_world.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo ${PIPESTATUS[@]}
5 0 1

*The command, echo ${PIPESTATUS[@]}, gets all elements of the array PIPESTATUS.* The first element in the array (5) is the exit status of the hello_world.sh 5 command. Similarly, the second element in the array (0) is the exit status of the grep “Hello World” command. Finally, the third element in the array (1) is the exit status of the grep “Hello Universe” command.

Of course, we can get the exit status of the command we’re interested in by using the corresponding index of the array. The index of the first element in the array is 0:

$ hello_wold.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo ${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]}
5 0 1

We must be aware that each command execution updates the PIPESTATUS variable:

$ hello_world.sh 5 | grep "Hello World" | grep "Hello Universe"
$ echo ${PIPESTATUS[2]}
1
$ echo ${PIPESTATUS[0]}
0

In this case, the PIPESTATUS[0] returned an exit status that’s different than our expectation. Because the execution of the command echo ${PIPESTATUS[0]} returned the exit status of the previous command, that’s echo ${PIPESTATUS[2]}. The behavior is like the one of the echo $? command. $? is the exit status of the previous command. Its value is updated in each command execution. Hence the echo ${PIPESTATUS[0]} command gives the same exit status with the echo $? command when we use no pipes:

$ hello_world.sh 5
Hello World
$ echo $?
5
$ hello_world.sh 5
Hello World
$ echo ${PIPESTATUS[0]}
5

4. Saving the Exit Status of Each Command

What if Bash had not supplied the PIPESTATUS variable for us? Well, while there’s life, there’s hope. In that case, we can save the exit status of each command and check it later:

$ { hello_world.sh 5; echo $? > exit_status_1; } | { grep "Hello World"; echo $? > exit_status_2; } | { grep "Hello Universe"; echo $? > exit_status_3; }
$ cat exit_status_1
5
$ cat exit_status_2
0
$ cat exit_status_3
1

In this example, we grouped each command with an echo $? command. Hence we created three command groups. We saved the exit status of each command to a file. For example, we saved the exit status of the command hello_world.sh 5 to the file exit_status_1. When we use command groups in a pipeline, the output of each group is redirected to the input of the next group. As we see, the exit status of each command is the same as the one we obtained using the PIPESTATUS variable.

5. The pipefail Option of the set Command

We already know that the exit status of a pipeline is the exit status of the last command in the pipeline. For example, as we saw earlier, the following pipeline exited with the exit status 0. However, the first command in the pipeline, hello_world.sh 5, exited with the exit status 5:

$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
0

Sometimes, we may want the pipeline to return the exit status of any command in the pipeline that exits with a nonzero value. We may use the pipefail option of the set command for this goal.

The set command is a built-in Linux command. It displays and sets the names and values of environment variables. It also has many options that enable and disable several behaviors in the shell. The pipefail option is one of them. It is disabled by default. In this case, the exit status of the pipeline is the exit status of the last command in the pipeline. However, if we enable it, the exit status of the pipeline is the exit status of the rightmost command in the pipeline that exits with a nonzero exit status. The set –o pipefail command enables this behavior while the set +o pipefail command disables this behavior:

$ set -o pipefail
$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
5
$ set +o pipefail
$ hello_world.sh 5 | grep "Hello World"
Hello World
$ echo $?
0

When we enable the pipefail option with the set –o pipefail command in the last example, the exit status of the pipeline is 5. It’s the exit status of the hello_world.sh script. However, when we disable the pipefail option with the set +o pipefail command, the exit status of the pipeline is the exit status of the last command in the pipeline, grep “Hello World”.

In order to show that the exit status of the pipeline is the exit status of the rightmost command in the pipeline that exits with a nonzero exit status, let’s run the following commands:

$ set -o pipefail
$ hello_world.sh 5 | grep "Hello Universe" | (grep Hello || true)
$ echo ${PIPESTATUS[@]}
5 1 0
$ hello_world.sh 5 | grep "Hello Universe" | (grep Hello || true)
$ echo $?
1

Here, the exit status of the commands hello_world.sh 5 and grep “Hello Universe” are both nonzero, 5 and 1, respectively. But the exit status of the pipeline is 1 since the grep “Hello Universe” is the rightmost command that has a nonzero exit status.

6. Conclusion

In this tutorial, we discussed several ways of getting the exit status of commands that are piped to another.

We saw that Bash has an array variable, PIPESTATUS, that stores the exit status of each command in the pipeline. We also mentioned about how we can store the exit status of each command in the pipeline. Finally, we discussed the pipefail option of the set command.