Linux Basics: Diagnose Bash Pipeline Failures
A pipeline can finish successfully even when an earlier stage failed. Bash offers two complementary tools: pipefail changes the pipeline’s overall status, and PIPESTATUS identifies the individual stages. Learn both before treating a zero status as proof that a multi-command operation worked.
These examples were tested on Linux with GNU Bash 5.2.37. Use Bash rather than sh. The failures below are deliberate and harmless. The scripts do not enable set -e, because they need to inspect nonzero results explicitly.
Prepare a scratch directory
You need Bash, a text editor, and the cat, grep, mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file in this directory before running it. The guards stop the shell if setup fails; invoking bash explicitly avoids changing executable permissions. No administrator access is needed.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Notice an incomplete producer
Save this as pipeline-producer.sh:
#!/usr/bin/env bash
produce() {
printf 'partial record\n'
return 7
}
set +o pipefail
produce | cat > /dev/null
status=$?
printf 'Default pipeline status: %d\n' "$status"
set -o pipefail
produce | cat > /dev/null
status=$?
printf 'With pipefail: %d\n' "$status"Run it:
bash pipeline-producer.shOutput:
Default pipeline status: 0
With pipefail: 7The function writes a record and then reports a simulated failure. cat consumes the text successfully, so the default pipeline status is zero. Turning on pipefail makes the earlier failure visible in the overall result.
Already-written output is not rolled back. A real producer can leave incomplete data even though its final status reports failure. Check status before treating generated output as complete. pipefail also does not stop the other stages automatically or decide how your script should recover.
2. Capture every stage before another command runs
Save this as pipeline-snapshot.sh:
#!/usr/bin/env bash
set -o pipefail
(exit 4) | (exit 9) | (exit 0)
snapshot=("$?" "${PIPESTATUS[@]}")
printf 'Overall: %s\n' "${snapshot[0]}"
printf 'Stages: %s, %s, %s\n' "${snapshot[1]}" "${snapshot[2]}" "${snapshot[3]}"Run it:
bash pipeline-snapshot.shOutput:
Overall: 9
Stages: 4, 9, 0Each parenthesized command exits with a chosen status, making the result independent of files or network services. With pipefail, the rightmost nonzero stage determines the overall status: nine here. “Rightmost” refers to its position in the pipeline, not which process happened to finish last.
The single array assignment saves $? and all three PIPESTATUS elements together. Their expansions happen before that assignment itself updates shell status. Do not save $? with one command and copy PIPESTATUS with another: the first assignment would already replace the stage information. Even a diagnostic printf is another command.
3. Recognize a valid no-match result
Save this as pipeline-no-match.sh:
#!/usr/bin/env bash
export LC_ALL=C
set -o pipefail
printf '%s\n' pear plum | grep '^apple$' | cat > /dev/null
stages=("${PIPESTATUS[@]}")
printf 'Stage statuses: %s\n' "${stages[*]}"
if (( stages[0] == 0 && stages[1] == 1 && stages[2] == 0 )); then
printf 'No matching records; input and output stages succeeded\n'
else
printf 'Unexpected pipeline result\n' >&2
exit 1
fiRun it:
bash pipeline-no-match.shOutput:
Stage statuses: 0 1 0
No matching records; input and output stages succeededFor grep, status one means no selected lines, not an input-reading error. This script accepts that particular outcome only after verifying that the surrounding stages succeeded. A blanket || true would lose that distinction and could conceal a genuine error.
The array follows pipeline order, starting at index zero. Quoted ${stages[*]} joins these numeric values for display; it is not a way to forward separate arguments. Check each command’s documented statuses when building your own policy.
Keep in mind
Short-circuiting consumers such as head can close their input early, leaving an upstream producer with a broken-pipe status. Decide whether that is expected for the particular workflow. pipefail provides evidence, not a universal error policy. When combining it with set -e, deliberately place handled failures in conditionals rather than relying on the shell to infer your intent.
Clean up
In the same terminal, remove only the named practice files, return to your original directory, and remove the empty scratch directory:
rm -- pipeline-producer.sh pipeline-snapshot.sh pipeline-no-match.sh
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirReferences
Acknowledgements
This post was created with assistance from AI.