bash-pitfalls: explain the reach of errexit suppression, with measurements.
commit 93b1f1577352ba46a1788fd20265b8578b7e9c26 · parent 0c4d3ffbad
· browse this tree
Ximianth, 2026-10-25 09:48:14 UTC
The old text said set -e couldn't be a strict mode without saying why. The why shapes the library twice over: every function checks its own exit codes, and the runner's fresh bash per case is load-bearing, since no fork sheds the suppression. Probed on bash 4.4 and 5.2 (identical): suppression from tested contexts is inherited by subshells, substitutions, and async commands; substitutions lack working errexit even called plainly; only a process boundary escapes.
diff --git a/docs/hacking/bash-pitfalls.md b/docs/hacking/bash-pitfalls.md index 043cb1c..2cc902d 100644 --- a/docs/hacking/bash-pitfalls.md +++ b/docs/hacking/bash-pitfalls.md @@ -158,6 +158,38 @@ We've been unable to use `set -e` as a strict mode (unfortunately). That means an awful lot of `cmd || return 1` is required. +The reason is a suppression rule with long reach. +`set -e` is ignored for any command whose failure is being tested: +the condition of an `if` or `while`, a command under `!`, +or any command of an `&&`/`||` list except the last. +So far, so documented. +The reach is that the suppression extends dynamically into every function *called* from such a position, +where `set -e` can neither be re-enabled nor even detected as off. +Library code must expect calls like `testmark_check ... || status=$?`, +so every function here has to assume `set -e` does nothing for it. + +The suppression is also inherited by forked children, where it likewise can't be shaken off. +Measured, on bash 4.4 and 5.2, with `set -e` on and `f() { false; echo survived; }`: +does `survived` print? + +| where `f` runs | called plainly | called from a tested context | +| --- | --- | --- | +| the current shell | no | yes | +| a subshell `( f )` | no | yes | +| an async command `f &` | no | yes | +| a command substitution `$(f)` | yes | yes | +| a process substitution `<(f)` | yes | yes | +| a fresh bash, running `set -e; f` | no | no | + +(The substitutions don't deliver a working `set -e` even when called plainly; +for command substitution that's documented behavior, bash clearing `-e` there outside posix mode.) + +Only a process boundary sheds the suppression. +This is why the runner gives every case a fresh bash process: +a case is promised a working `set -euo pipefail`, +and since callers legitimately write `testmarktest_run docs || exit 1`, +no fork of the harness's shell could keep that promise. + ### treat unset variables as fatal We turned `set -u` on in our library code, as a strict mode to catch as many bugs as possible.