Linux Basic — set -o pipefail

3 minute read

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

Updated: