Traps that never fired, and traps that fired twice
- #traps
- #bash-compat
- #bugfix
Traps are the shell's error handling. If they misfire, scripts don't crash — they quietly do the wrong thing. This round fixed four ways huck got them wrong inside functions, and all four were the silent kind.
Cleanup handlers that never ran
A RETURN trap is how a function cleans up after itself — close the handle,
remove the temp file, log the exit. huck ran the function and skipped the
handler:
# before
$ huck -c 'cleanup() { echo "closing $1"; }
f() { trap "cleanup db" RETURN; echo querying; }
f; echo next'
querying
next# after
$ huck -c 'cleanup() { echo "closing $1"; }
f() { trap "cleanup db" RETURN; echo querying; }
f; echo next'
querying
closing db
nexthuck was asking the wrong question at the wrong moment: is trap inheritance
turned on? — checked when the function returned. bash asks a different
question, and asks it on the way in: when a function starts, bash puts the
caller's RETURN trap aside for the duration, then runs whatever trap is
installed when the function returns. A trap the function set for itself
was never inherited from anyone, so nothing has to be turned on for it to run.
Same rule, moved to the right place, and three other behaviours fell out of it
for free — the caller's trap is now invisible to trap -p inside the body,
a trap the body installs outlives the call, and a body that resets the trap
gets the caller's back.
$? that lied
This one is the reason the round is worth a post. Any script with an ERR
trap got a corrupted exit status after every failing command:
# before
$ huck -c 'trap "echo error hit" ERR; false; echo "status=$?"'
error hit
status=0$? was the echo inside the trap, not the false that triggered it. Every
if [ $? -ne 0 ] after a failure took the wrong branch — silently, in exactly
the scripts careful enough to install an error handler.
# after
$ huck -c 'trap "echo error hit" ERR; false; echo "status=$?"'
error hit
status=1A trap action has to be transparent to $?: save the status, run the handler,
put the status back. huck already did that for DEBUG traps and not for the
other two.
Errors reported twice
The last pair was over-reporting. A function that failed announced itself once for the failing command inside, and again for the call:
# before
$ huck -c 'trap "echo ERROR" ERR; check() { false; }; check; echo "rc=$?"'
ERROR
ERROR
rc=0# after
$ huck -c 'trap "echo ERROR" ERR; check() { false; }; check; echo "rc=$?"'
ERROR
rc=1Two separate rules, both from bash. A function doesn't inherit the caller's
ERR trap unless you ask for it with set -E — same entry-time move as
RETURN. And bash decides whether a failure is trap-worthy before running
the command, so a function that installs an ERR trap isn't caught by the
trap it just installed. set -e is deliberately unaffected by that rule: a
failing function still exits the script whether or not a handler is armed.
What's covered, and what isn't
Each fix ships with a harness that runs the same fragments through huck and real bash and asserts byte-identical output — 54 new fragments across the two files, on top of a suite that is now 262 harnesses, all green.
Four neighbouring gaps turned up while fixing these and are filed rather than
smuggled in: exit inside a trap action doesn't exit the shell, DEBUG traps
need the same entry-time treatment, ERR still fires twice inside { } /
for / case, and a sourced script never fires RETURN at all. They all
touch machinery shared across features, which is a different-sized job than a
bug-fix round.