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 | trueis 0 andtrue | falseis 1. The shell waits for every stage but keeps one number. PIPESTATUSkeeps them all, one per stage, left to right. It is overwritten by the very next command, so it must be read at once: theechoon the same line reads it, and a secondechowould see the firstecho's status.- A failure in the middle disappears.
grepfound nothing and exited 1;sortsorted nothing and exited 0; the pipeline is 0. Sols -l | grep alma | wc -lsucceeds whether or not any file matched, and the only sign is the0it prints. set -o pipefailreports the rightmost stage that failed, so the same pipeline is now 1.- And that is its sharp edge.
yes | head -n 1did exactly what it was asked:headprinted one line and exited, closing the pipe, and the next write killedyeswith SIGPIPE. Underpipefailthat is status 141 (128 + 13, the signal's number), a "failure" nobody made. A script withpipefailon that pipes intohead,grep -qor 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, becauserun_examples.py --check | tail -1reportstail's status and not the check's. Its docstring walks throughPIPESTATUSagainst zsh'spipestatustoo. - 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¶
- bash manual, Pipelines ↗ —
PIPESTATUS,pipefail, andtimein front of a pipeline - zsh manual, Options ↗ —
PIPE_FAIL - fish language, special variables ↗ —
statusandpipestatus