Linux Basic — set -o pipefail
set -o pipefail
By default, the exit code of a pipeline is the exit code of the last command — even if curl failed in the middle. set -o pipefail fixes that: the pipeline returns non-zero if any command in it failed.
It’s the third piece of set -euo pipefail.
Default Pipeline Behaviour
false | true | echo done
echo "$?"
# 0 ← only echo's exit code is reported
That’s almost always wrong — you wanted to know about the false failure.
With pipefail:
set -o pipefail
false | true | echo done
echo "$?"
# 1 ← the leftmost non-zero is returned
Now you can pipe curl … | jq … safely. If curl fails, the script knows.
What pipefail Returns
“The exit status of a pipeline is the exit status of the last (rightmost) command to exit with a non-zero status, or zero if all commands exit successfully.”
So if multiple commands fail, you get the rightmost failing one’s code. To inspect all of them, use PIPESTATUS:
set -o pipefail
false | false | true
echo "rc=$?"
echo "PIPESTATUS=${PIPESTATUS[*]}"
# rc=1
# PIPESTATUS=1 1 0
The head / SIGPIPE Trap
This bites the unprepared:
set -o pipefail
# Take only first 10 lines of a long stream
yes | head -n 10
echo "$?"
# 141 ← yes died from SIGPIPE (128 + 13) when head closed the pipe!
head closes the pipe after reading 10 lines. The upstream command (yes) gets a SIGPIPE on its next write and dies. With pipefail, that “death” counts as a failure.
Two fixes:
A. Tolerate SIGPIPE explicitly
yes | head -n 10 || true
B. Trap SIGPIPE (rarely needed)
trap '' PIPE
This makes the script ignore SIGPIPE — risky, since real errors might also be hidden.
C. Avoid head by computing the limit upstream
ls -1 | sort | awk 'NR <= 10' # no SIGPIPE — awk reads to EOF
In practice, || true after the head line is the simplest fix. Don’t disable pipefail for the whole script just to suppress one warning.
Real-World Pattern — Safe curl | jq
Without pipefail:
set -e
data=$(curl -sS https://api.example.com/x | jq .field)
# If curl 404s → curl exits 22 but jq sees empty input → jq exits 0
# Script proceeds with empty $data 🎉
With pipefail:
set -euo pipefail
data=$(curl -sS https://api.example.com/x | jq .field)
# curl 22 → pipefail propagates → set -e aborts
Always pair pipefail with -e if you want crash-on-failure.
Per-Pipeline Opt-Out
Sometimes you specifically don’t want pipefail to bite — e.g., the SIGPIPE case above, or a grep that legitimately returns 1 on no-match.
Option 1 — || true after the pipeline
some_cmd | grep -q ERROR || true
Option 2 — Toggle inside a block
set +o pipefail
some_cmd | head -1
set -o pipefail
Option 3 — Use if
if some_cmd | grep -q ERROR; then
handle_error
fi
if already disables set -e for the controlling command; pipefail still applies, but a 1 from grep is the expected “no match” code that means “false.”
grep Returns 1 on No Match
This catches people:
set -euo pipefail
# ❌ Aborts the script if "ERROR" not found
errors=$(grep ERROR app.log)
# ✅ Tolerate no match
errors=$(grep ERROR app.log || true)
# ✅ Or branch explicitly
if errors=$(grep ERROR app.log); then
handle "$errors"
fi
Same logic applies to find (returns non-zero on permission errors), diff (1 = differ, 2 = error).
Putting It All Together
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
fetch() {
local url=$1
# If curl fails, pipefail + set -e abort the script
curl -sS --fail "$url" | jq -r '.items[]'
}
main() {
local items
items=$(fetch "https://api.example.com/data") || {
rc=$?
echo "FAIL: fetch returned $rc" >&2
return "$rc"
}
echo "$items"
}
main "$@"
curl --fail makes curl exit 22 on HTTP 4xx/5xx. Pipefail catches that. set -e aborts. The || { … } reports a friendlier error.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
Just set -e (no pipefail) and piping curl/jq |
set -euo pipefail |
middle-of-pipe failures invisible |
Surprised by exit 141 from … \| head |
… \| head \|\| true |
head closes pipe → upstream SIGPIPE |
grep in if with pipefail = abort |
grep in if is fine — exit handled by if |
only standalone grep-no-match aborts |
Reading $? after pipeline expecting first cmd’s code |
check PIPESTATUS[0] |
$? is rightmost failing code |
| Toggling pipefail mid-pipeline | toggle around the whole pipeline | flag changes between commands have no effect |
Next → trap Command