Bash gotcha: pipeline returns the wrong exit code
Bash pipelines return only the exit status of the last command unless `pipefail` is set. Enable it to propagate failures from any stage.
Error String
Pipeline exits 0 even though a command failed
Tldr
By default `a | b` returns `b`'s exit code, even if `a` failed. Set `set -o pipefail` so the pipeline fails if any stage fails. Combine with `set -e` for fail-fast scripts.
Cause
A `curl | bash` style invocation that fails the download but succeeds the bash exits 0 — and your CI thinks everything is fine.
Repro
false | true; echo "exit: $?" # prints 0
set -o pipefail
false | true; echo "exit: $?" # prints 1Fix
#!/usr/bin/env bash
set -euo pipefail
if ! curl -fsSL https://example.com/installer.sh | bash; then
echo "install failed" >&2
exit 1
fiExplanation
Combine `set -euo pipefail` and `curl -f` (fail on HTTP errors) for installers. Use `${PIPESTATUS[@]}` to inspect every stage individually when you need finer control.
Faq
Q
Is pipefail POSIX?
A
No. It exists in Bash, ksh and zsh; plain `sh` scripts need `PIPESTATUS`-free workarounds such as temporary files.
Q
Why does my grep pipeline now fail the script?
A
`grep` exits 1 when it finds nothing, and pipefail propagates that. Append `|| true` to that element if no match is a valid outcome.
Deep Dive
Heading
A pipeline reports only its last command by default
Body
Without `set -o pipefail`, `cmd1 | cmd2` exits with the status of `cmd2`, so a failing producer is hidden whenever the consumer succeeds — `curl bad-url | tee out.log` looks fine. With `pipefail` the pipeline returns the rightmost non-zero status instead, which is what makes `set -euo pipefail` the common safety preamble. `PIPESTATUS` holds every element status individually and must be copied immediately, because the next command overwrites it.
Heading
When pipefail causes surprising failures
Body
Commands that exit non-zero as normal behaviour trip it: `grep` returns 1 when nothing matched, and any producer writing into `head` gets SIGPIPE once the consumer stops reading, giving status 141. Handle these deliberately — `grep -q pattern file || true` when no match is acceptable, or restructure so the pipeline ends with the command whose status you care about. Blanket `|| true` on a whole pipeline throws away the safety you enabled pipefail for, so scope it to the one element that is allowed to fail.
Checklist
Copy `status=("${PIPESTATUS[@]}")` immediately after the pipeline to inspect each element.
Add `set -o pipefail` when a producer failure must fail the script.
Allow expected non-zero exits explicitly rather than disabling pipefail.
Remember status 141 means SIGPIPE from a consumer such as `head` closing early.