Bash / Editorial solution guide
Find pipeline and set -e status pitfalls
Investigate a script that reports success after partial work by separating stage statuses, expected failures, and contextual errexit behavior.
Read-only knowledge base · No public comments
Recognize the symptoms
A report file contains plausible partial data, the final command returns zero, and the script continues despite set -e. The same helper appears to stop when run directly but continues when called by if. Preserve the precise caller structure: moving the helper into a tiny isolated command can remove the context responsible for the bug.
Understand the likely causes
- The default pipeline result reflects the final stage, not all required stages. pipefail changes aggregation but not the business meaning of nonzero results. errexit has grammar and invocation exceptions, and ERR has related exclusions. A later log command or local declaration can also replace the status that the script intended to inspect.
1. Reproduce with harmless commands in the same structure
Record whether the helper is called normally, tested by if, negated, or part of an AND/OR list or pipeline. Keep the relevant options and interpreter version. Replace real file publication or administration with synthetic producers and explicit return values. Inspect both the required operation and its caller; a function body alone cannot explain contextual errexit. Do not add more global options before understanding which command actually supplied the zero result. Separate diagnostics from partial data so the investigation does not accidentally make the report look complete.
bash -e -c 'work() { false; printf "continued-inside-function\n"; }; if work; then printf "condition-succeeded\n"; fi'
continued-inside-function
condition-succeeded
This intentional pitfall is non-runnable in the reader, but its harmless output was checked. The failed command does not abort the tested function; its final printf returns success.
2. Capture all pipeline stages before any logging
Immediately assign stages=("${PIPESTATUS[@]}") after the relevant foreground pipeline. Any intervening command can replace the array. Label each saved position with its stage meaning and compare it against that program’s exit-status contract. A search returning one for no matches may be acceptable; an unreadable input is different. If a consumer intentionally stops early, interpret an upstream broken pipe in that specific design rather than automatically classifying every nonzero value as the same failure. Never accept output solely because the consumer parsed it successfully.
set +e
{ printf 'partial\n'; return_status=6; ( exit "$return_status" ); } | { while IFS= read -r line; do :; done; }
stages=("${PIPESTATUS[@]}")
printf 'producer=%s consumer=%s\n' "${stages[0]}" "${stages[1]}"
producer=6 consumer=0
The synthetic producer emits one line then exits six in its final child command. The saved vector retains six and zero without publishing any real artifact.
3. Handle required operations explicitly
Use if required_operation; then ... else status=$?; ... fi around work whose failure changes correctness. Inside a reusable function, do not rely on its caller leaving errexit active. Return a documented nonzero status when required work fails, and ensure a later diagnostic does not overwrite that decision. Separate local declaration from a status-sensitive command substitution. pipefail remains useful for required pipelines, but identify expected absence, optional stages, and validation before choosing the final aggregate policy. A global switch cannot decide those application requirements.
4. Keep partial output away from the completion boundary
For a real transform, write to a distinct owned lab destination and validate it before the documented publication step. The source must not be truncated by redirecting back to the same path. If any required stage failed, preserve the original failure and reject the partial artifact even if it is syntactically valid. Test producer failure, consumer failure, no-match, and the actual function-call context independently. Review trap output as supplementary evidence because ERR inheritance and exclusions can hide the message the author expected.
Verify the fix
Assert the script’s final status and whether a completed artifact was accepted for every controlled failure. Verify saved stage statuses before diagnostics and repeat the exact if/list context that exposed the issue. A successful test should prove both correct rejection of partial work and unchanged handling of documented expected absence.
Keep it from recurring
- Document required versus optional stages, capture statuses adjacent to operations, and keep explicit failure returns inside helpers. Retain regression cases for tested functions and partial producer output. Record supported Bash behavior instead of presenting set -e, pipefail, and ERR together as a universal exception system.