Pipelines, pipefail, and Pipeline Exit Status
The vertical bar is one of the shell's defining features: it turns small programs into data-processing systems. But production pipelines have subtleties—multiple exit statuses, concurrent execution, subshells, early consumers, and stderr that does not enter the pipe unless explicitly redirected.
Learning objectives
By the end of this lesson
- Explain how Bash wires adjacent pipeline commands.
- Distinguish pipeline data flow from stderr flow.
- Inspect individual stage results with PIPESTATUS.
- Use pipefail to avoid false-success pipelines.
- Recognize subshell and SIGPIPE behavior that affects production scripts.
1. A pipeline connects stdout to the next stdin
For a pipeline such as producer | filter | consumer,
Bash creates pipes and launches commands so each stage can stream
data to the next.
flowchart TD A["producer"] -->|"stdout"| P1["pipe"] P1 -->|"stdin"| B["filter"] B -->|"stdout"| P2["pipe"] P2 -->|"stdin"| C["consumer"] A -. "stderr" .-> T["terminal / inherited stderr"] B -. "stderr" .-> T C -. "stderr" .-> T
stderr is not part of the pipeline by default. Each stage inherits the shell's stderr unless you redirect it.
2. Pipelines stream; they do not require whole intermediate files
Pipeline stages can run concurrently. A producer can write data while the next command is already consuming it. This enables low-latency processing and avoids many temporary files.
printf '%s\n' 'INFO api ready' 'WARN cache slow' 'INFO worker ready' |
grep '^INFO' |
awk '{print $2}' |
sort -u
This composability depends on each command honoring a useful stream contract. Tools that write clean records to stdout and diagnostics to stderr integrate naturally.
3. By default, pipeline status usually follows the last command
Without pipefail, Bash uses the exit status of the
final command as the status of a normal foreground pipeline (subject
to negation with !). That can hide an upstream failure.
set +o pipefail
bash -c 'exit 7' | cat
printf 'pipeline status without pipefail = %d\n' "$?"
cat successfully reaches end-of-file and exits zero, so
the overall pipeline can appear successful even though the producer
failed.
4. PIPESTATUS preserves each stage's most recent status
Bash stores an array named PIPESTATUS containing the
exit statuses of commands in the most recently executed foreground
pipeline.
set +o pipefail
bash -c 'printf "data\n"; exit 7' |
grep data |
cat
statuses=("${PIPESTATUS[@]}")
printf 'stage statuses: %s\n' "${statuses[*]}"
Capture PIPESTATUS immediately. Running another
command—even a diagnostic printf—updates shell status
state and can replace the values you intended to inspect.
5. pipefail makes upstream failure visible
With set -o pipefail, a pipeline returns zero only if
all commands succeed. If one or more commands fail, the pipeline
status is the value from the rightmost failing command.
set -o pipefail
if bash -c 'printf "data\n"; exit 7' | grep data | cat; then
printf 'pipeline succeeded\n'
else
status=$?
printf 'pipeline failed with status %d\n' "$status" >&2
fi
For scripts where every pipeline stage matters,
pipefail is usually an important reliability setting.
It prevents a successful final filter from masking an earlier
failure.
6. pipefail and set -e solve different problems
pipefail changes how pipeline status is calculated.
set -e (errexit) influences when Bash exits after
certain failing commands. They are related but not interchangeable,
and set -e has context-dependent exceptions.
set -o pipefail
if producer | transformer | consumer; then
printf 'all pipeline stages succeeded\n'
else
printf 'pipeline failed\n' >&2
exit 1
fi
Explicit control flow such as if remains valuable even
in scripts that use strict-mode settings. It documents which
failures are expected, recoverable, or fatal.
7. |& includes stderr in the pipe
Bash provides |& as shorthand that pipes both stdout
and stderr from the command on the left into the next command. It is
roughly equivalent to merging stderr into stdout before the pipe.
{
printf 'normal record\n'
printf 'warning record\n' >&2
} |& sed 's/^/[combined] /'
Use this only when combining the streams is intentional. If stdout
carries structured data, |& can make the stream
impossible for downstream parsers to consume reliably.
8. Pipeline stages can run in subshell environments
A common surprise is assigning a variable inside a loop that receives pipeline input:
count=0
printf '%s\n' a b c |
while IFS= read -r line; do
((count += 1))
done
printf 'count after pipeline = %d\n' "$count"
In typical Bash behavior, the loop runs in a subshell environment because it is a pipeline element, so changes do not persist in the parent shell. A redirection avoids that problem:
count=0
while IFS= read -r line; do
((count += 1))
done < <(printf '%s\n' a b c)
printf 'count after redirection = %d\n' "$count"
Bash also has a lastpipe option with specific
conditions, but relying on explicit data-flow structure is usually
easier to reason about.
9. Early consumers can cause SIGPIPE upstream
Commands such as head may stop after receiving enough
data. An upstream writer that continues writing can then receive
SIGPIPE because no reader remains.
set -o pipefail
# Depending on the producer and timing, the producer may observe a broken pipe.
yes "record" | head -n 3
statuses=("${PIPESTATUS[@]}")
printf 'statuses: %s\n' "${statuses[*]}"
This is not always a “real error” in the business operation; it can
be a normal consequence of intentionally stopping consumption early.
Under pipefail, however, such a status can make the
overall pipeline non-zero. Robust scripts distinguish expected early
termination from unexpected failure.
10. Know when a temporary file is clearer than a pipeline
Pipelines are ideal for streaming transformations, but a temporary file may be better when you need to:
- inspect or preserve intermediate evidence after failure,
- run multiple independent consumers over the same expensive output,
- validate a complete artifact before continuing,
- retry one stage without rerunning an expensive producer.
Good shell design is not a contest to use the most pipes. Choose the data-flow structure that makes failure and recovery easiest to reason about.
11. Hands-on lab: detect an upstream failure
Create a three-stage pipeline where the producer intentionally fails
after emitting valid-looking data. Compare behavior with and without
pipefail.
mkdir -p "$HOME/devops-academy/bash/chapter03/lesson04"
cd "$HOME/devops-academy/bash/chapter03/lesson04"
cat > producer.sh <<'EOF'
#!/usr/bin/env bash
printf '%s\n' 'INFO api ready' 'WARN cache slow' 'INFO worker ready'
printf 'producer: source was incomplete\n' >&2
exit 23
EOF
set +o pipefail
bash producer.sh | grep '^INFO' | wc -l
printf 'without pipefail: %d\n' "$?"
set -o pipefail
if bash producer.sh | grep '^INFO' | wc -l; then
printf 'unexpected success\n'
else
pipeline_status=$?
stage_statuses=("${PIPESTATUS[@]}")
printf 'with pipefail: %d\n' "$pipeline_status"
printf 'recent statuses snapshot: %s\n' "${stage_statuses[*]}"
fi
Verification checklist
12. Knowledge check
Question 1. Does | pipe stderr by
default?
Question 2. What problem does
set -o pipefail solve?
Question 3. Why can assignments inside
producer | while read ... disappear afterward?
13. Summary
Pipelines are concurrent stream networks. The pipe connects stdout
to the next stdin; stderr remains separate unless you deliberately
merge it. Bash's default pipeline status can hide upstream failures,
while PIPESTATUS exposes individual stages and
pipefail makes the overall result stricter. Also
account for subshell scope and legitimate SIGPIPE behavior from
early consumers.
14. Further reading
-
GNU Bash Reference Manual — Pipelines and
PIPESTATUS. -
GNU Bash Reference Manual —
pipefailand shell options. - POSIX Shell Command Language — pipelines.
-
Linux
pipe(7)andsignal(7)manual pages.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.