| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; examples create files in /tmp/bash-pipe-lab |
| Scope | Bash pipelines: stdout with |, stdout and stderr with |&, default vs pipefail exit status, ${PIPESTATUS[*]}, subshell behavior, SIGPIPE with early consumers, functions in pipelines, and brief lastpipe / strict-mode notes. Does not cover full FD redirection or set -euo pipefail rules. |
| Related guides | Shell scripting course hub |
A Bash pipe connects commands in a chain:
command1 | command2The | operator sends command1's stdout to command2's stdin. You use that pattern constantly for filtering logs, counting lines, and chaining small tools.
Basic pipelines are simple, but error handling can be surprising: Bash normally reports only the last command's exit status, pipeline commands often run in subshells, and early consumers such as head can trigger SIGPIPE. The examples below show how pipefail and PIPESTATUS handle those cases.
How a Bash Pipe Works
Bash starts the commands in a pipeline without waiting for each previous stage to finish. The commands can therefore run concurrently: one command can write data while the next reads it. Data flows as a byte stream on stdout, not as a finished variable or file unless you add redirection.
Count lines from stdin:
printf 'line1\nline2\n' | wc -l2Filter lines that contain ERROR from a sample log:
mkdir -p /tmp/bash-pipe-lab
printf '2026-09-10 INFO ok\n2026-09-10 ERROR fail\n' > /tmp/bash-pipe-lab/app.log
grep ERROR /tmp/bash-pipe-lab/app.log | wc -l1grep sends matching lines to wc, which counts newline-terminated records on stdin.
A pipe sends data to the next command's standard input; it does not turn that data into command-line arguments. Commands such as grep, sort, and wc commonly read stdin. Commands that require filenames or other positional arguments may need xargs, command substitution, or another approach. For broader redirection background, see Linux redirection and pipes.
Only stdout enters the pipe by default. stderr still goes to the terminal unless you redirect it or use |&.
Pipe stdout and stderr with |&
|& is a Bash shorthand for 2>&1 |. Both stdout and stderr from the left command feed the right command.
Compare a plain pipe with |& when the producer writes to both streams:
{ echo stdout; echo stderr >&2; } | wc -lstderr
1Only the stdout line reached wc. The stderr line printed to the terminal.
Merge both streams into the pipe:
{ echo stdout; echo stderr >&2; } |& wc -l22>&1 | produces the same count on Bash 5.2:
{ echo stdout; echo stderr >&2; } 2>&1 | wc -l2|& is Bash-specific. Use 2>&1 | when a script must stay POSIX-portable beyond Bash-only syntax.
Pipeline Exit Status Without pipefail
By default, a pipeline's exit status comes from the last command, even when an earlier stage failed. See Bash exit codes for the general $? model; pipelines add their own rules.
false | true
echo $?0The first stage failed, but $? is 0 because true was the rightmost command and it succeeded.
Enable set -o pipefail
pipefail changes which status the shell returns for a pipeline. Enable it for the current shell or script:
set -o pipefailpipefail has long been supported by Bash and several other shells. POSIX.1-2024 now standardizes pipefail, but shells implementing older POSIX editions may not support it. Check the target shell before relying on it in portable scripts. Bash's PIPESTATUS array remains Bash-specific.
Test the same failing first stage:
bash -c 'set -o pipefail; false | true; echo exit=$?'exit=1With pipefail, a failure in any stage can make the pipeline fail. If you also use set -e, the shell may exit when a pipeline returns a non-zero status, depending on context. Full set -e exceptions belong in a dedicated strict-mode guide.
What Status Does pipefail Return?
When pipefail is on and one or more stages fail, Bash returns the status of the rightmost command that exited with a non-zero status. If every stage succeeds, the pipeline status is 0.
All stages succeed:
bash -c 'set -o pipefail; true | true | true; echo exit=$?'exit=0When multiple stages fail with different statuses, the pipeline returns the rightmost non-zero status:
bash -c 'set -o pipefail; (exit 7) | (exit 3) | true; echo exit=$?'exit=3Both the first and second commands failed, but the pipeline returned 3 because the second command was the rightmost command with a non-zero status.
pipefail does not replace reading per-stage status when you need to know which command failed. Use PIPESTATUS for that.
When a Non-Zero Exit Status Is Not an Error
Before deciding how pipefail should treat a pipeline, check what non-zero statuses mean for the commands involved. For example, grep returns 1 when a valid search finds no matches:
printf 'INFO ok\n' | grep ERROR | wc -l
printf 'status=%s\n' "${PIPESTATUS[*]}"0
status=0 1 0grep returned 1 because it found no matching lines. That does not necessarily mean grep malfunctioned. If pipefail is enabled, that 1 also makes the overall pipeline status non-zero. Your script must decide whether "no matches" is an expected result or an error.
An invalid regular expression is a different case: GNU grep reports an actual error, typically exit status 2, rather than the normal no-match status.
Inspect Every Stage with PIPESTATUS
${PIPESTATUS[@]} is Bash-specific. It stores one exit status for each command in the most recently executed foreground pipeline, in left-to-right order. POSIX.1-2024 standardized pipefail, but the PIPESTATUS array is not POSIX.
false | true | true
printf 'stages=%s\n' "${PIPESTATUS[*]}"stages=1 0 0Index 0 belongs to false, followed by the two true commands.
Capture PIPESTATUS Before It Changes
PIPESTATUS is overwritten as soon as another command runs, including a simple assignment or echo.
Run a pipeline, then read statuses immediately:
bash -c 'false | true | true; echo immediate=${PIPESTATUS[*]}; ls / >/dev/null; echo after_ls=${PIPESTATUS[*]}'immediate=1 0 0
after_ls=0ls replaced the array with its own single status.
A common mistake is reading $? before PIPESTATUS:
bash -c 'set -o pipefail; yes | head -n 1; status=$?; printf "PIPESTATUS=%s status=%s\n" "${PIPESTATUS[*]}" "$status"'y
PIPESTATUS=0 status=141status captured the pipeline exit (141 from yes under pipefail), but the assignment already replaced PIPESTATUS with its own status (0). Capture the array first when you need every stage:
bash -c 'set -o pipefail; yes | head -n 1; stages=("${PIPESTATUS[@]}"); printf "stages=%s\n" "${stages[*]}"'y
stages=141 0Pipelines and Subshells
Each command in a pipeline normally runs in a subshell environment. Variable changes inside the pipeline do not persist in the parent shell after the pipeline finishes.
count=0
printf 'a\nb\nc\n' | while read -r line; do count=$((count + 1)); done
echo "count=$count"count=0The while loop ran in a subshell, so the parent count never changed. Read a file line by line in Bash shows redirection and process substitution patterns that keep the loop in the current shell.
With job control disabled, shopt -s lastpipe can run the last pipeline command in the current shell:
bash -c 'shopt -s lastpipe; count=0; printf "a\nb\n" | while read -r line; do count=$((count+1)); done; echo count=$count'count=2lastpipe depends on shell options and job-control state. For Bash scripts, process substitution such as while read …; done < <(command) is often clearer when the loop must modify variables in the current shell.
Why pipefail Can Report 141 with head
When a downstream command stops reading early — head is the classic example — an upstream writer may receive SIGPIPE and exit with status 141 (128 + 13).
bash -c 'set -o pipefail; yes | head -n 1; stages=("${PIPESTATUS[@]}"); printf "stages=%s\n" "${stages[*]}"'y
stages=141 0head succeeded (0). yes exited with 141 because the pipe closed. With pipefail, the pipeline status becomes 141.
When the downstream command intentionally stops reading early, as head does here, an upstream 141 may be expected rather than evidence that the producer itself malfunctioned. Check the pipeline and ${PIPESTATUS[*]} before deciding to ignore it.
Pipelines Inside Functions
A function called as the left stage of a pipeline also runs in a subshell. Assignments inside the function do not update parent-shell variables.
myvar=1
myfunc() { myvar=2; printf 'inside\n'; }
myfunc | cat
echo "myvar=$myvar"inside
myvar=1The function executed, its stdout traveled through the pipe, and its variable assignment did not survive in the parent. GNU Bash confirms that each command in a multi-command pipeline normally executes in its own subshell, subject to the lastpipe exception.
For logic that must mutate caller state, avoid putting the function on the left side of a pipe. See Bash functions for return values and output capture patterns.
When a Pipeline Is Not the Best Fit
Pipelines excel at streaming text between filters. They are a poor fit when the right-hand command must update shell state or when you need every stage's status after complex branching.
| Need | Prefer |
|---|---|
| Loop in the current shell over command output | while read …; done < <(command) with process substitution |
| Load all lines into an array | mapfile with process substitution — see Bash mapfile |
| Debug intermediate data | tee to a file, or run stages separately |
| Compare two command outputs as files | diff <(cmd1) <(cmd2) — see Bash command substitution for related capture patterns |
tee duplicates a stream but does not fix exit-status handling by itself. You still need pipefail and immediate PIPESTATUS capture when failures matter.
Practical Example: Log Filtering with pipefail and PIPESTATUS
This example filters ERROR lines, writes them to errors.txt, and counts them. With pipefail enabled, a failure in grep or tee is not hidden just because wc succeeds.
cd /tmp/bash-pipe-lab
set -o pipefail
grep ERROR app.log | tee errors.txt | wc -l
stages=("${PIPESTATUS[@]}")
printf 'stages=%s\n' "${stages[*]}"1
stages=0 0 0All three stages succeeded. stages=0 0 0 maps to grep, tee, and wc in order.
tee wrote the same line to errors.txt:
cat errors.txt2026-09-10 ERROR failIn production scripts, check stages before assuming the filter worked. If grep finds no matching lines, it returns 1 even though wc -l successfully prints 0. With pipefail, that non-zero grep status becomes the pipeline status. Decide whether "no matches" is an expected condition before treating every non-zero pipeline status as an error.
An invalid regular expression is a different case: GNU grep reports an actual error rather than the normal no-match status.
Strict mode (set -euo pipefail) builds on these rules. Full set -e exceptions and when to enable strict mode belong in a dedicated strict-mode guide. This article only previews how pipefail changes the pipeline status that set -e may act on.
Common Pipeline Failure Patterns
| Symptom | Likely cause | Fix |
|---|---|---|
Pipeline exits 0 but an early command failed |
Default status uses the last stage | set -o pipefail |
PIPESTATUS shows only one value |
Another command ran after the pipeline | Capture "${PIPESTATUS[@]}" immediately |
Loop counter stays 0 after while read in a pipe |
Pipeline subshell | Use < <(command) or lastpipe with care |
Function variable unchanged after func | … |
Function ran in a subshell | Restructure or use stdout/return |
Status 141 after head or another early-exiting consumer |
Upstream SIGPIPE |
Check ${PIPESTATUS[*]}; may be expected when the reader stopped early |
| stderr missing from the pipe | Plain | pipes stdout only |
2>&1 | or |& |
tee succeeds even though another stage failed |
tee does not determine the pipeline's failure policy |
Enable pipefail or inspect PIPESTATUS |
Summary
Bash pipes stream stdout from one command into the next with |. stderr stays on the terminal unless you merge it with |& or 2>&1 |. By default, $? reflects only the last stage, which can hide an earlier failure.
set -o pipefail makes the pipeline fail when any stage fails, returning the rightmost non-zero status. POSIX.1-2024 now standardizes pipefail, but Bash's ${PIPESTATUS[*]} array remains Bash-specific. Capture it immediately after the pipeline, before the next command runs.
Pipeline stages usually run in subshells, so while read loops and functions on the left side of a pipe do not update parent variables. Early consumers such as head can leave upstream writers with status 141; check ${PIPESTATUS[*]} before treating that as a script error. Some commands, including grep, use non-zero statuses for ordinary conditions such as "no matches." When you need current-shell loops or arrays, use process substitution or mapfile instead of a plain pipeline.
References
- GNU Bash Manual — Pipelines
- GNU Bash Manual —
pipefail - GNU Bash Manual — Bash Variables /
PIPESTATUS - POSIX — Pipelines
Frequently Asked Questions
Does a pipe include stderr?
No. A plain | connects stdout to stdin only. stderr still prints to the terminal unless you redirect it. Bash |& merges stdout and stderr into the pipe, equivalent to 2>&1 |.
What does pipefail change?
Without pipefail, the pipeline exit status is the status of the last command. With pipefail, the status is the rightmost non-zero exit status among the stages, or zero if every stage succeeded. POSIX.1-2024 standardizes pipefail, but shells implementing older POSIX editions may not support it.
How do I get every stage's exit code?
Read ${PIPESTATUS[@]} immediately after the pipeline finishes. Index 0 is the leftmost command. Use "${PIPESTATUS[*]}" to print all stages on one line. The next command you run replaces the array. PIPESTATUS is Bash-specific.
Why does a pipeline succeed when an earlier command fails?
Default Bash behavior uses only the last command's status. false | true exits 0 because true succeeded. Enable pipefail or inspect PIPESTATUS when middle failures matter.
Why can head trigger a non-zero upstream status?
When head stops reading, the pipe closes and the upstream writer may get SIGPIPE, often reported as exit status 141. That is common for intentional early truncation. Check ${PIPESTATUS[*]} before deciding whether to treat it as an error.

