What does `set -o pipefail` change about the exit status of a bash pipeline, and when several stages fail, which one's status do you actually get?
answer
- changes the value, aborts nothing
- rightmost non-zero, not the first
- shell option, not a per-line flag
- surfaces grep-1 and SIGPIPE-141 too
- absent from dash and strict POSIX sh
basics
~20 sWith set -o pipefail, a bash pipeline returns the status of the rightmost command that exited non-zero, or 0 if every stage succeeded. Without it, only the last stage's status counts, so earlier failures vanish.
solid answer
~40 s`set -o pipefail` changes the value a pipeline produces: instead of the last stage's status, you get the status of the **rightmost stage that exited non-zero**, and 0 only when every stage succeeded. So `(exit 3) | (exit 4) | true` returns 4, and `(exit 6) | (exit 5)` returns 5 — it is the rightmost failure, not the first one. It is a shell option, so it applies to every pipeline executed afterwards in that shell, and it can be turned off again with `set +o pipefail`. The practical cost is that non-zero no longer reliably means "something went wrong": `grep` returns 1 for no matches and a producer killed by SIGPIPE returns 141, and pipefail promotes both into pipeline failures you now have to handle deliberately.
code
bash · 9 lines#!/usr/bin/env bash
set -o pipefail
(exit 3) | (exit 4) | true; echo "three then four -> $?" # 4
(exit 6) | (exit 5); echo "six then five -> $?" # 5
true | true; echo "all succeeded -> $?" # 0
set +o pipefail
(exit 3) | true; echo "pipefail off -> $?" # 0go deeper
Know that set -o pipefail makes a failing stage anywhere in a pipeline show up in the exit status, instead of only the last command counting, and that it is one line in the script header.
State the rule exactly — rightmost non-zero stage, 0 when all succeed — and show you know it is a shell option affecting every later pipeline, not a modifier on one line.
Talk about the cost side: pipefail promotes grep's no-match 1 and a SIGPIPE 141 into failures, so be ready to say how you scope the option off or annotate the pipelines that legitimately tolerate a non-zero stage.
Frame it as a policy question: strict-by-default with annotated exceptions is reviewable, ad-hoc opt-in is not. Weigh that against the portability constraint that the option does not exist in dash-based /bin/sh.
## What the option does `pipefail` is a shell option, toggled with `set -o pipefail` / `set +o pipefail`. It rewrites one rule: the exit status of a pipeline becomes the status of the **last (rightmost) command that exited with a non-zero status**, or zero when all commands exit successfully. ```bash set -o pipefail false | true; echo $? # 1 (exit 3) | (exit 4) | true; echo $? # 4 (exit 6) | (exit 5); echo $? # 5 true | true; echo $? # 0 ``` Read those last two carefully — they are the discriminating detail interviewers probe. Bash does not report the first failure, and it does not report the largest status. It scans the stages left to right and keeps the status of the *last* one that was non-zero. In `(exit 6) | (exit 5)` that is 5, even though 6 failed first and is numerically larger. ## Scope and lifetime Because it is a shell option, `pipefail` applies to every pipeline the shell runs from that point on, not to one line. It is inherited by subshells the shell forks, but not by separate programs it executes — a child script with its own shebang starts with the option off unless it sets it itself. A common pattern is a narrowly scoped opt-out: ```bash set -o pipefail # ... strict region ... set +o pipefail noisy_producer | head -n 5 # tolerate an early-exit reader here set -o pipefail ``` Portability matters: `pipefail` is a bash option (also present in ksh93 and zsh), and older strict POSIX shells such as `dash` do not have it — `set -o pipefail` there fails outright. A script that needs it must declare `#!/usr/bin/env bash`, not `#!/bin/sh`. ## What it buys you The motivating case is any pipeline whose final stage cannot realistically fail: `tee`, `wc -l`, `sort`, `jq`, `column -t`. Without pipefail, those swallow the producer's failure entirely and a broken build reports success. With it, the producer's status reaches the caller and normal control flow works again: ```bash set -o pipefail if ! make build 2>&1 | tee build.log; then echo "build failed" >&2 exit 1 fi ``` This is also why the option shows up in the conventional strict-mode header alongside the other `set` flags: it closes the specific hole where a failure is real but invisible. ## What it costs you The flip side is that pipefail is indiscriminate — it promotes *every* non-zero stage, including the ones that were never errors: - `grep` exits 1 when it simply found no matches. Under pipefail, `log_reader | grep WARN | wc -l` now returns 1 on a clean log. - A producer killed by SIGPIPE because a downstream `head` quit early records 141, and pipefail makes the whole pipeline 141. - Tools that use exit codes as data (`diff` returning 1 for "files differ", `cmp`, some linters) turn every meaningful result into a pipeline failure. So enabling pipefail is not free: it converts a silent-failure problem into a false-failure problem you must resolve case by case, typically with `|| true` on the pipelines that legitimately tolerate a non-zero stage, or by inspecting `${PIPESTATUS[@]}` to accept some stages' statuses and reject others. ## What it does not do Three confusions are worth naming explicitly. It does not abort anything. `pipefail` only changes the *value* a pipeline yields; whether a non-zero value stops the script is a different mechanism entirely. It does not stop the pipeline early. All stages still run concurrently to completion; a failure in stage one does not prevent stage two from processing what it already received. It does not populate `PIPESTATUS` — that array is maintained for every foreground pipeline whether pipefail is on or off. What pipefail changes is only which single number lands in `$?`. In fact after a pipeline you can have `$?` and the last element of `PIPESTATUS` disagree, which is a good way to demonstrate that they are separate mechanisms: ```bash set -o pipefail false | true echo "$? / ${PIPESTATUS[*]}" # 1 / 1 0 ``` ## Review heuristic Turn pipefail on by default for anything that runs unattended, then deliberately mark the pipelines that are allowed to have a non-zero stage. That ordering — strict by default, exceptions annotated — is far easier to review than the opposite, where every unmarked pipeline might be hiding a failure.
- Under pipefail, `(exit 6) | (exit 5)` returns 5 rather than 6. Why?Because the rule picks the *rightmost* stage that exited non-zero, not the first failure and not the largest number. Bash walks the stages left to right and keeps the last non-zero status it saw, so the second stage's 5 wins. If you need to know that the first stage failed with 6, read `${PIPESTATUS[0]}` instead of `$?`.
- Does enabling pipefail in a script affect scripts it invokes?No. It is a shell option of the current shell. Subshells forked by that shell inherit it, but any external program — including another script with its own shebang — starts a fresh shell that must set the option itself. That is why the option belongs in every script's own header rather than being assumed from the caller.
- Name one pipeline where you would deliberately switch pipefail off.Any pipeline that ends in an early-exiting reader, such as `long_producer | head -n 10`. The producer gets SIGPIPE when `head` closes the pipe and records 141, which pipefail turns into a pipeline failure even though the behaviour is exactly what you asked for. Either scope the option off around it or append `|| true` for that line.
saying these in an interview costs you the question
- Says pipefail returns the first or leftmost failing stage
- Claims pipefail aborts the script when a stage fails
- Thinks pipefail stops the remaining stages from running
- Believes PIPESTATUS only exists when pipefail is enabled
- Assumes set -o pipefail works in /bin/sh on any system