Bash & the POSIX Shell Module 6 – Redirection, pipes and substitution
Pipelines, PIPESTATUS and pipefail
Connect commands with | and |&, read every stage's exit status with PIPESTATUS, catch failures with pipefail, and see why yes | head returns 141.
What you will learn
- Connect commands with | and |&, and explain why each stage runs as a separate process
- Read every stage's status from a copy of PIPESTATUS, and predict the status pipefail reports
- Recognise the 141 of a writer stopped by SIGPIPE, and decide stage by stage when to accept it
Before you start
What you’ll learn · Before you start
What you will learn
- Connect commands with | and |&, and explain why each stage runs as a separate process
- Read every stage's status from a copy of PIPESTATUS, and predict the status pipefail reports
- Recognise the 141 of a writer stopped by SIGPIPE, and decide stage by stage when to accept it
Before you start
On this page
false | true ends with status 0, although its first command failed. A pipeline: Commands joined by | or |&, each one's standard output flowing into the next one's standard input, all running at the same time. Its exit status is the last command's unless pipefail is set. reports
the status of its last command only, so a failure in any earlier stage is invisible unless you look for it. Bash keeps
every stage’s status in the array PIPESTATUS, and set -o pipefail makes the whole pipeline fail when any stage
fails: in the session below, false | true | (exit 4) left PIPESTATUS at 1 0 4 and returned 4.
You will connect commands with | and |&, read each stage’s status before the next command replaces it, predict
what pipefail reports, and tell a real failure from the 141 a pipeline returns when its reader stops early. The
streams of the previous lesson are all the background you need.
A pipeline’s status is its last command’s
cmd1 | cmd2 connects the standard output: Descriptor 1, where a command writes its results: the terminal, unless > file, >> file or a pipe points it somewhere else. Also called stdout. More in “Redirection: stdin, stdout, stderr, /dev/null” of cmd1 to the standard input of cmd2
through a pipe the shell creates, and a longer pipeline does the same for every pair of neighbours. When the pipeline
ends, the shell reports one status, and it is the last command’s:
"""Pipelines, PIPESTATUS and pipefail: the status of a pipeline, every stage's status, and the pipefail option.
The session shows:
1. A pipeline's status is its last command's: false | true gives 0.
2. With pipefail, the status is that of the last failing stage.
3. PIPESTATUS holds every stage's status, but only until the next command.
4. Each stage is a separate process (its own subshell), so variables set there are lost.
5. |& sends stderr as well.
6. dash 0.5.13.5 supports pipefail.
"""
from session import run
run("""
# A pipeline's status is its last command's
false | true
echo "false | true: $?"
# A three-stage pipeline: only the last status counts
false | true | (exit 4); echo "status: $?"
# PIPESTATUS holds every stage's status (copy it at once!)
false | true | (exit 4); p=("${PIPESTATUS[@]}"); echo "PIPESTATUS: ${p[0]} ${p[1]} ${p[2]}"
# The next command replaces PIPESTATUS
false | true | (exit 4); p=("${PIPESTATUS[@]}"); echo "Saved: ${p[*]}"; echo "After echo: ${PIPESTATUS[*]}"
# With pipefail, the status is the last non-zero
set -o pipefail
false | true | (exit 4); echo "With pipefail: $?"
# The last failing stage, not the first
(exit 2) | true | (exit 7); echo "pipefail picks the rightmost failure: $?"
set +o pipefail
# Each stage runs in a subshell: variables are lost
count=0
echo one | while read line; do count=$((count+1)); done
echo "count after the pipe: $count"
# |& sends stderr as well (short for 2>&1 |)
ls /no-such-dir |& cat
# dash supports pipefail (POSIX.1-2024)
dash -c 'set -o pipefail; false | true; echo "dash pipefail: $?"'
""") Output
# A pipeline's status is its last command's
$ false | true
$ echo "false | true: $?"
false | true: 0
# A three-stage pipeline: only the last status counts
$ false | true | (exit 4); echo "status: $?"
status: 4
# PIPESTATUS holds every stage's status (copy it at once!)
$ false | true | (exit 4); p=("${PIPESTATUS[@]}"); echo "PIPESTATUS: ${p[0]} ${p[1]} ${p[2]}"
PIPESTATUS: 1 0 4
# The next command replaces PIPESTATUS
$ false | true | (exit 4); p=("${PIPESTATUS[@]}"); echo "Saved: ${p[*]}"; echo "After echo: ${PIPESTATUS[*]}"
Saved: 1 0 4
After echo: 0
# With pipefail, the status is the last non-zero
$ set -o pipefail
$ false | true | (exit 4); echo "With pipefail: $?"
With pipefail: 4
# The last failing stage, not the first
$ (exit 2) | true | (exit 7); echo "pipefail picks the rightmost failure: $?"
pipefail picks the rightmost failure: 7
$ set +o pipefail
# Each stage runs in a subshell: variables are lost
$ count=0
$ echo one | while read line; do count=$((count+1)); done
$ echo "count after the pipe: $count"
count after the pipe: 0
# |& sends stderr as well (short for 2>&1 |)
$ ls /no-such-dir |& cat
ls: /no-such-dir: No such file or directory
# dash supports pipefail (POSIX.1-2024)
$ dash -c 'set -o pipefail; false | true; echo "dash pipefail: $?"'
dash pipefail: 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pipestatus_session.py
false | true gave 0 because true came last. The three-stage pipeline ended with 4, the status of (exit 4), and
the 1 from false left no trace in $?. The Bash manual says it in one sentence: “The exit status of a pipeline is
the exit status of the last command in the pipeline”, unless pipefail is on.
The stages are separate programs that run at the same time. Bash starts every stage as its own process, each in its
own subshell: A copy of the shell that runs commands apart from it, as Bash does for each command of a pipeline. It starts with the shell's variables, and every change it makes to them is lost when it exits. More in “Reading files line by line with read”, joins them with pipes, and waits until all of them have ended before it
reports the status. Data moves while they run: each stage reads whatever the stage before it has written so far, and
a writer whose pipe is full waits for its reader. That is how yes | head -n 1, later in this lesson, can end at all,
although yes never stops writing by itself.
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The pipeline starts. Each stage runs in its own subshell, a copy of the shell that cannot change the parent's variables.
- Stage 1 fails. false returns 1. Its output (nothing here) goes to stage 2.
- Stage 2 succeeds. true returns 0, even though stage 1 failed.
- Stage 3 fails. (exit 4) returns 4. It is the last stage.
- Without pipefail. The pipeline's status is the last stage's: 4. The failures of stages 1 and 3 look the same.
- With pipefail. set -o pipefail keeps 4 here, but shows 1 when the first stage alone fails. PIPESTATUS holds 1 0 4.
Because each stage is a subshell, no stage can change the variables of the shell that started the pipeline. The
session’s count stayed 0 after echo one | while read line; do …; done, since the loop counted in its own copy of
count. The lesson on reading files line by line shows how to feed a loop
without losing what it sets.
A pipe carries descriptor 1 only. ls /no-such-dir |& cat sent the error of ls through cat as well: |& is Bash’s
short form of 2>&1 |, which its NEWS lists among the changes of Bash 4.0. POSIX defines only |, so a script for sh
writes 2>&1 | in full.
Errors that never enter the pipe
Wrong: ./build.sh | grep -c error to count a build’s errors when build.sh writes them on standard error: they go
to the terminal, and the count is 0. Right: ./build.sh 2>&1 | grep -c error, or |& in place of 2>&1 | in Bash.
Checkpoint
Check: Pipeline status and stages
PIPESTATUS holds every stage’s status
After a pipeline, Bash sets the array PIPESTATUS to the statuses of its commands in order: ${PIPESTATUS[0]} is the
first stage, ${PIPESTATUS[1]} the second, and so on. It lasts only until the next command. Bash sets it again after
simple commands, subshells and the compound commands [[ ]] and (( )), so the next echo, test or assignment
replaces it with an array of one element, its own status. The session read it twice after the same pipeline: copied
at once into p, it held 1 0 4; read again after an echo, it held 0.
Key idea
Copy PIPESTATUS on the same line as the pipeline, before anything else runs, with p=("${PIPESTATUS[@]}"), and let
every later check read the copy.
The copy is an ordinary array, so the usual array forms work on it: ${#p[@]} is the number of stages, 3 here, and
${p[*]} prints them all on one line. Walking it from index 0 upwards finds the stage that failed first, which
neither $? nor pipefail can tell you. PIPESTATUS is one of Bash’s own variables: POSIX defines no such variable, and
sh has no arrays to hold one, so a script for sh that needs more than the last status relies on pipefail.
Checkpoint
Check: PIPESTATUS
pipefail makes the pipeline’s status the rightmost failure
set -o pipefail changes which status the pipeline reports: the status of the rightmost command that exited with a
non-zero status, or 0 when every command succeeded. In the session it kept 4 for false | true | (exit 4), and
(exit 2) | true | (exit 7) returned 7, not 2. pipefail changes nothing about how the stages run. Every stage still
runs to its own end, and only the number reported afterwards differs.
Turn it on where a failed producer must not look like success: a script that builds a report with
extract | sort | uniq -c should not deliver an empty report because extract failed while sort and uniq succeeded on
no input. POSIX.1-2024 says the setting that counts is the one in force when the pipeline starts, so
set -o pipefail goes before the pipelines it should cover, usually near the top of the script.
The option has been in Bash since 3.0, whose NEWS introduced it, and POSIX.1-2024 made it part of the standard.
dash 0.5.13.5 has it too: the session’s dash -c 'set -o pipefail; false | true; echo "dash pipefail: $?"' printed
1, so a script that uses pipefail also works under a current dash.
pipefail does not pick the first failure
(exit 2) | true | (exit 7) returns 7, not 2: pipefail keeps the rightmost non-zero status. When you need to know
which stage failed first, read a copy of PIPESTATUS.
Checkpoint
Check: pipefail
SIGPIPE and the 141 from a healthy pipeline
head -n 1 exits as soon as it has printed one line. When the writer before it tries to write again, nothing reads
the pipe any more, and the system sends the writer SIGPIPE: Signal 13, sent to a process that writes to a pipe nobody has open for reading any more, for example after head has exited. By default it ends the writer, and Bash reports that as status 128 + 13 = 141.. Left to that signal’s default
action, the writer terminates. For a command that a fatal signal stopped, Bash sets the status to 128 plus the
signal’s number; SIGPIPE is signal 13, so the writer’s status is 141:
"""SIGPIPE and pipefail: when a reader stops early, the writer receives SIGPIPE.
The session shows:
1. yes | head -n 1: head exits after one line, yes gets SIGPIPE.
2. Without pipefail, the status is 0 (head succeeded).
3. With pipefail, the status is 141 (128 + 13, where SIGPIPE is signal 13).
4. PIPESTATUS shows "141 0": yes was killed, head succeeded.
5. A healthy pipeline can look failed because of this, so decide when 141 is acceptable.
"""
from session import run
run("""
# yes writes forever; head stops after one line
yes | head -n 1
# Without pipefail, status is 0 (head's status)
yes | head -n 1; echo "status: $?"
# With pipefail, status is 141 (yes was killed by SIGPIPE)
set -o pipefail
yes | head -n 1; echo "status: $?"
# PIPESTATUS shows both stages' statuses
yes | head -n 1; p=("${PIPESTATUS[@]}"); echo "PIPESTATUS: ${p[*]}"
# 141 = 128 + 13 (SIGPIPE)
echo "128 + 13 = $((128 + 13))"
# A pipeline with cat | head also shows 141
cat /dev/urandom | head -c 100 | wc -c; p=("${PIPESTATUS[@]}"); echo "PIPESTATUS: ${p[*]}"
set +o pipefail
# How to accept 141 as success
set -o pipefail
yes | head -n 1; rc=$?
if [ "$rc" -eq 0 ] || [ "$rc" -eq 141 ]; then echo "OK (status $rc)"; else echo "FAILED (status $rc)"; fi
set +o pipefail
""") Output
# yes writes forever; head stops after one line
$ yes | head -n 1
y
# Without pipefail, status is 0 (head's status)
$ yes | head -n 1; echo "status: $?"
y
status: 0
# With pipefail, status is 141 (yes was killed by SIGPIPE)
$ set -o pipefail
$ yes | head -n 1; echo "status: $?"
y
status: 141
# PIPESTATUS shows both stages' statuses
$ yes | head -n 1; p=("${PIPESTATUS[@]}"); echo "PIPESTATUS: ${p[*]}"
y
PIPESTATUS: 141 0
# 141 = 128 + 13 (SIGPIPE)
$ echo "128 + 13 = $((128 + 13))"
128 + 13 = 141
# A pipeline with cat | head also shows 141
$ cat /dev/urandom | head -c 100 | wc -c; p=("${PIPESTATUS[@]}"); echo "PIPESTATUS: ${p[*]}"
100
PIPESTATUS: 141 0 0
$ set +o pipefail
# How to accept 141 as success
$ set -o pipefail
$ yes | head -n 1; rc=$?
y
$ if [ "$rc" -eq 0 ] || [ "$rc" -eq 141 ]; then echo "OK (status $rc)"; else echo "FAILED (status $rc)"; fi
OK (status 141)
$ set +o pipefail
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sigpipe_session.py
yes | head -n 1 printed one y every time. Without pipefail its status was 0, head’s; with pipefail it was 141, and
PIPESTATUS read 141 0: yes was killed, head succeeded. cat /dev/urandom | head -c 100 | wc -c showed the pattern one
stage further left, 141 0 0, with wc counting exactly 100 bytes. Nothing went wrong in either pipeline. The reader had
what it needed, and SIGPIPE is how the system stops a producer whose output nobody reads.
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Both start. The shell starts yes and head as separate processes, with a pipe between them.
- yes writes "y". yes writes "y\n" to the pipe forever, one line at a time.
- head reads one line. head reads from the pipe, prints "y" and exits with status 0.
- yes writes again. yes writes another "y\n", but the pipe's reader is gone.
- SIGPIPE. The kernel sends SIGPIPE to yes, whose default action ends the process. Bash reports a command ended by a signal as 128 plus its number, 13, so 141.
- The result. PIPESTATUS holds 141 0. Without pipefail, the status is 0. With pipefail, 141.
Under pipefail, that 141 reaches $? and every test of the pipeline’s status. The session’s last block accepted it
for the whole pipeline: it kept the status in rc and treated 0 and 141 as success. That rule is coarse once a pipeline
has more stages, because a 141 can come from any of them. PIPESTATUS is exact: accept 141 only from the stage right
before a reader that stops early by design, such as head, and only when that reader returned 0. Any other non-zero
entry is a real failure.
Key idea
Under pipefail, 141 usually means that a reader stopped early on purpose and the writer before it was told to stop. Decide stage by stage, from PIPESTATUS, where a 141 is expected.
To pick lines out of a log by hand rather than in a pipeline, the filter lines tool keeps or removes the lines that contain your words and shows both sets side by side.
Checkpoint
Check: SIGPIPE and 141
Practise: report the stage that failed
Build run_checked in your own terminal: a function that runs a three-stage pipeline and names the first stage that
failed.
- Define three functions as stand-in stages:
generateprints five lines, one of them starting with#;filterdrops that line withgrep -v '^#';countrunswc -l. Make each fail on request: when the variableFAIL_ATholds its stage number, the stage reads its input to the end withcat > /dev/nulland returns 3. - Write
run_checked. It runsgenerate | filter | count, copies PIPESTATUS on the same line, and walks the copy from index 0. At the first non-zero entry it prints on standard error the stage’s number, name and status, such asstage 2 (filter) failed with status 3, and returns that status; when every entry is 0 it returns 0. - Run it with
FAIL_ATempty, then 1, 2 and 3. Its status must be 0, then 3 three times, and each message must name the stage you chose. - Now let
countreturn 3 at once, without reading its input, and runFAIL_AT=3again. The message may name stage 2 with status 141. Explain why: filter wrote into a pipe whose reader had already gone.
The questions below use pipelines the lesson did not run. Work out each status before you open the explanation:
Practice that adapts to you
Practise this lesson
10 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: what status does a pipeline return, how do you see every stage’s status, and what does pipefail change (fresher to 2–5 years)?
A pipeline’s status is its last command’s: false | true returns 0 because true came last. PIPESTATUS holds every
stage’s status as an array, ${PIPESTATUS[0]} for the first stage; copy it on the same line, because the next
command replaces it. With set -o pipefail, the pipeline returns the status of the rightmost command that failed, or
0 when all of them succeeded, so false | true returns 1. pipefail has been in Bash since 3.0 and is part of
POSIX.1-2024.
The two harder questions are about a monitor that stopped alerting on a busy day although the error line was there, and a clean-up step that emptied the very file it was meant to tidy. For each, work out what every stage was doing at the moment it ended before you open the first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A monitor runs under set -o pipefail and pages when if dump_journal | grep -q 'disk full' succeeds. On quiet days it pages as it should. On a busy day the dump was 100,000 lines long with 'disk full' near the top, and no page fired. Why, and how would you fix the check?
-
Fresher to mid
A clean-up step runs sort users.txt | uniq > users.txt to remove duplicate lines. Afterwards users.txt is empty, every time, although sort and uniq both exit 0. What happened, and how would you remove the duplicates in place safely?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which status does a pipeline return by default?
Its last command's, so false | true returns 0 and the failure of false is lost unless you look for it.
-
Why does a variable set inside a pipeline stage vanish afterwards?
The stages run at the same time as separate processes, each in a subshell, so whatever a stage sets is gone when the pipeline ends.
-
Which stream does | carry, and how do you send standard error through too?
Only standard output. |& or 2>&1 | adds standard error to what the next command reads.
-
How do you keep every stage's status of a pipeline?
Copy the PIPESTATUS array on the same line, before anything else runs, with p=("${PIPESTATUS[@]}"), and read the copy, because the next command replaces it.
-
What does set -o pipefail change?
The pipeline returns the status of its rightmost command that failed, or 0 when every stage succeeded. Bash has had it since 3.0, and POSIX.1-2024 and dash 0.5.13.5 have it too.
-
Why can a healthy pipeline such as cmd | head -n 1 report 141 under pipefail?
When the reader stops early, the writer is killed by SIGPIPE, signal 13, and Bash reports 128 plus 13. Decide from PIPESTATUS, stage by stage, where a 141 is expected.
Key takeaways
- A pipeline reports its last command’s status, so
false | truereturns 0 and the failure offalseis lost unless you look for it. - The stages run at the same time as separate processes, each in a subshell, so a variable set inside a stage does not
survive the pipeline, and
|carries standard output only unless|&or2>&1 |adds standard error. PIPESTATUSholds every stage’s status until the next command replaces it, so copy it at once withp=("${PIPESTATUS[@]}")and read the copy.set -o pipefailmakes the pipeline return the status of its rightmost failing command, or 0, and it is in Bash since 3.0, in POSIX.1-2024 and in dash 0.5.13.5.- When a reader stops early, the writer before it is killed by SIGPIPE and Bash reports its status as 141, 128 + 13, so under pipefail a healthy pipeline can look failed; decide from PIPESTATUS where a 141 is expected.
Terms in this lesson
- pipeline
- Commands joined by | or |&, each one's standard output flowing into the next one's standard input, all running at the same time. Its exit status is the last command's unless pipefail is set.
- SIGPIPE
- Signal 13, sent to a process that writes to a pipe nobody has open for reading any more, for example after head has exited. By default it ends the writer, and Bash reports that as status 128 + 13 = 141.
References
- Bash Reference Manual: Pipelines (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: The Set Builtin (pipefail) (Chet Ramey (GNU Bash maintainer))
- Bash Reference Manual: Bash Variables (PIPESTATUS) (Chet Ramey (GNU Bash maintainer))
- Bash NEWS: the new features of each release (Chet Ramey (GNU Bash maintainer))
- POSIX.1-2024: Shell Command Language (2.9.2 Pipelines) (The Open Group and IEEE)
- Linux man-pages: signal(7) (Linux man-pages project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress