Learn › Bash Redirection and File Descriptors › Status is part of the data contract
Status is part of the data contract. Try the examples, inspect their output, and explain the result. Practice feedback does not award XP or mastery.
A generator writes the beginning of a report and fails. Its consumer reads those bytes successfully. Should the complete operation count as successful? For a report exporter, the answer is usually no: valid partial bytes are not proof of a complete report.
An exit status is the small integer a command returns to its caller. Zero normally means success. A nonzero value needs interpretation according to that command's documented interface. For example, a search finding no match can be an expected result rather than a broken system.
Use Bash 5.2 or later in a private directory. These examples deliberately create failures. Their inspection commands report the evidence instead of relying on your interactive prompt to show it.
In your managed Bash practice terminal
Run camp-redirection 08 to enter this chapter's private directory. While this lab machine is running, the same command returns to it after reconnecting; camp-redirection 08 --fresh creates a new one without deleting earlier files. Run the code blocks there and use cd to leave.
Without pipefail, a foreground pipeline normally reports the last component's status. Bash also supplies the array PIPESTATUS, an indexed list with one entry per component in left-to-right order. snapshot=(...) stores several values in another indexed list. "${snapshot[@]}" expands its entries in that same order for the final printf.
This example turns pipefail off. Predict the pipeline's overall status and each stage status when the producer returns 7 but cat succeeds. Save PIPESTATUS before another command replaces it.
(
set +o pipefail
{ printf 'partial\n'; exit 7; } | cat
snapshot=("$?" "${PIPESTATUS[@]}")
printf 'overall=%s producer=%s consumer=%s\n' "${snapshot[@]}"
)
Expected output
partial
overall=0 producer=7 consumer=0
The outer parentheses keep the pipefail setting inside one subshell. The braces group the producer commands. In this pipeline they execute in another shell environment, so exit 7 does not exit your terminal. Array positions begin at zero. Here, position zero stores the overall result, followed by the two component results.
pipefail makes the result the rightmost nonzero component status, or zero if every component succeeds. It does not automatically stop other components, erase partial output, or choose the earliest failure in time.
Compare overall status with producer 7 and consumer 4; pipefail reports the rightmost nonzero pipeline status, not the largest number.
(
set -o pipefail
{ printf 'partial\n'; exit 7; } | { cat; exit 4; }
snapshot=("$?" "${PIPESTATUS[@]}")
printf 'overall=%s producer=%s consumer=%s\n' "${snapshot[@]}"
)
Expected output
partial
overall=4 producer=7 consumer=4
pipefail is also specified by POSIX.1-2024. Older installed shells may lack it, so verify the actual interpreter rather than calling it universally available or Bash-only.
Even a successful builtin replaces PIPESTATUS. The : builtin performs no work and succeeds, which makes the loss easy to see.
The later colon command replaces PIPESTATUS; inspect the printed array to see why pipeline evidence must be captured before another command.
( exit 7 ) | cat; :; printf 'now: %s\n' "${PIPESTATUS[*]}"
Expected output
now: 0
If you saved only $? and then read PIPESTATUS, your assignment has already replaced the array. Use one assignment, as demonstrated above, when you need both forms of evidence.
The reserved word ! turns an overall zero status into one and an overall nonzero status into zero. It does not rewrite the individual component statuses.
(
set +o pipefail
! false | true
snapshot=("$?" "${PIPESTATUS[@]}")
printf 'inverted=%s first=%s last=%s\n' "${snapshot[@]}"
( exit 6 ) &
job=$! launch=$?
if wait "$job"; then result=0; else result=$?; fi
printf 'launch=%s completed=%s\n' "$launch" "$result"
)
The lines report inverted=1 first=1 last=0 and launch=0 completed=6. The surrounding subshell contains the option change and owns the background job. Starting that job successfully differs from its completion. $! identifies the recently started background job. Save it immediately, then collect its completion status with wait in that same subshell.
set -e, also called errexit, has exceptions for tested conditions and other contexts. A function called as an if condition runs with these exit rules ignored within its body.
bash -c 'set -e; build() { false; printf "continued inside function\n"; }; if build; then printf "condition succeeded\n"; fi'
Both lines print, and the child Bash exits successfully. The final printf made the function successful despite the earlier failure. This is a demonstration of a documented rule, not a dependable exporter design. Explicitly capture and return important failures, even when a project also uses set -e.
Try a variation: keep published.txt containing previous. Send a producer's partial output to another file. If either pipeline component fails, preserve the published file and print both statuses. Do not read the solution until you can state the acceptance rule.
printf 'previous\n' > published.txt
{ printf 'candidate\n'; exit 6; } | cat > candidate.txt
stages=("${PIPESTATUS[@]}")
if (( stages[0] == 0 && stages[1] == 0 )); then
mv candidate.txt published.txt
else
printf 'rejected: %s %s\n' "${stages[0]}" "${stages[1]}"
fi
cat published.txt
The result is rejected: 6 0, followed by previous. The recovery separates collection from acceptance. Retain the candidate while diagnosing this practice failure. A later chapter adds safe temporary-file creation and validated replacement for a real exporter.
Change the producer's failure to a consumer failure and verify rejection again. Then make both succeed. Explain which status your wrapper should return and why. A useful solution preserves evidence and applies a stated policy; a single green status is not sufficient evidence.
References: Bash pipeline status, Bash set builtin.
Practice Chapter 8. Status is part of the data contract in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.