Skip to content

A pipeline reports its last command

Level: 101 · you have typed ls -l | grep alma | wc -l and trusted $? afterwards

One line: A pipeline's exit status is its last command's, so a grep that finds nothing in the middle of … | grep … | sort still reports success. bash keeps every stage's status in PIPESTATUS, zsh in pipestatus (counted from 1), and fish in $pipestatus. set -o pipefail makes a failure anywhere count in bash and zsh, and it makes yes | head -n 1 fail too. fish has no pipefail at all.

Measured

The book example is ls -l | grep alma | wc -l, which prints 3. The count is only half of what the pipeline tells the shell, and the other half is its status:

Verified output of exit_status_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ false | true; echo "status $?"
status 0

$ true | false; echo "status $?"
status 1

$ false | true | true; echo "PIPESTATUS ${PIPESTATUS[*]}"
PIPESTATUS 1 0 0

$ printf "alma\ndarkfi\n" | grep nosuchword | sort; echo "status $?"
status 0

$ set -o pipefail; printf "alma\ndarkfi\n" | grep nosuchword | sort; echo "status $?"; set +o pipefail
status 1

$ set -o pipefail; yes | head -n 1; echo "status $? PIPESTATUS ${PIPESTATUS[*]}"; set +o pipefail
y
status 141 PIPESTATUS 141 0
  • Only the last command counts. false | true is 0 and true | false is 1. The shell waits for every stage but keeps one number.
  • PIPESTATUS keeps them all, one per stage, left to right. It is overwritten by the very next command, so it must be read at once: the echo on the same line reads it, and a second echo would see the first echo's status.
  • A failure in the middle disappears. grep found nothing and exited 1; sort sorted nothing and exited 0; the pipeline is 0. So ls -l | grep alma | wc -l succeeds whether or not any file matched, and the only sign is the 0 it prints.
  • set -o pipefail reports the rightmost stage that failed, so the same pipeline is now 1.
  • And that is its sharp edge. yes | head -n 1 did exactly what it was asked: head printed one line and exited, closing the pipe, and the next write killed yes with SIGPIPE. Under pipefail that is status 141 (128 + 13, the signal's number), a "failure" nobody made. A script with pipefail on that pipes into head, grep -q or anything else that stops reading early will report errors that are not errors.

On a Mac

Nothing changes: the key above is shared, and macOS's /bin/bash 3.2 printed the same bytes as bash 5.2 on Ubuntu. PIPESTATUS and pipefail are both old enough to be in 3.2.

In zsh and fish

zsh has both ideas under different spellings, and the bash spelling is not an error:

Verified output of exit_status_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ false | true; echo "status $?"
status 0

$ false | true | true; echo "pipestatus $pipestatus"
pipestatus 1 0 0

$ false | true; echo "first stage: ${pipestatus[1]}"
first stage: 1

$ false | true; echo "bash spelling: [${PIPESTATUS[0]}]"
bash spelling: []

$ setopt pipefail; printf "alma\ndarkfi\n" | grep nosuchword | sort; echo "status $?"; unsetopt pipefail
status 1

${PIPESTATUS[0]} prints nothing because zsh's array is lowercase pipestatus and its arrays start at 1. The uppercase name is simply an unset variable. A script moved from bash to zsh that checks ${PIPESTATUS[0]} therefore sees an empty string on every run, success or failure. setopt pipefail is the zsh spelling of set -o pipefail.

fish names it $pipestatus and $status, and rejects the two bash spellings outright:

Verified output of exit_status_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ false | true; echo "status $status"
status 0

$ false | true | true; echo "pipestatus $pipestatus"
pipestatus 1 0 0

$ true | false; echo "status $status, pipestatus $pipestatus"
status 1, pipestatus 0 1

$ printf "alma\ndarkfi\n" | grep nosuchword | sort; echo "pipestatus $pipestatus"
pipestatus 0 1 0

$ yes | head -n 1; echo "pipestatus $pipestatus"
y
pipestatus 141 0

$ false; echo $?
fish: $? is not the exit status. In fish, please use $status.
false; echo $?
             ^
(exit status 127)

$ set -o pipefail
Fish does not have shell options. See `help fish_for_bash_users`.
set: -o: unknown option
(exit status 2)

$ printf "alma\ndarkfi\n" | grep nosuchword | sort; set -l stages $pipestatus; if string match -qv 0 -- $stages; echo "a stage failed: $stages"; end
a stage failed: 0 1 0

$? is not a variable in fish. It is a parse error, reported before the line runs, with a message that says what to type instead. There is no pipefail option either: set -o is not an option of fish's set. To ask "did any stage fail?", test $pipestatus yourself, as the last command above does. string match -qv 0 succeeds if any element is something other than 0. Copy $pipestatus into a variable first, because the test is itself a command and replaces it.

The same table, then:

bash zsh fish
last command's status $? $? $status
every stage ${PIPESTATUS[@]}, from 0 $pipestatus, from 1 $pipestatus, from 1
a failure anywhere counts set -o pipefail setopt pipefail no option — test $pipestatus

If you are coming from another library

  • This library's own gate runner is the long version of this page. tools/check_all.py runs every gate without a pipe, because run_examples.py --check | tail -1 reports tail's status and not the check's. Its docstring walks through PIPESTATUS against zsh's pipestatus too.
  • Python. stdin, stdout and pipes ↗ is the same pipe seen from a program in the middle of it.
  • Encodings. A pipe is not a terminal ↗ covers the other thing that changes when output goes into a pipe instead of to the screen.

See also