Skip to content

executor: the ERR trap reports a failing command, not a propagating status (#729) - #739

Merged
berrym merged 1 commit into
masterfrom
fix/729-err-trap-per-failure
Aug 16, 2026
Merged

executor: the ERR trap reports a failing command, not a propagating status (#729)#739
berrym merged 1 commit into
masterfrom
fix/729-err-trap-per-failure

Conversation

@berrym

@berrym berrym commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Closes #729.

The rule

The ERR pseudo-signal notifies that a command failed. lush fired on any non-zero status that reached a statement walker -- a different thing -- and the gap showed in four shapes.

1. Propagation was re-reported (the filed defect)

$ lush -c "trap 'echo T' ERR; for i in 1; do for j in 1; do false; done; done"
T
T
T                # bash, zsh: one T

The count tracked nesting depth rather than failures. Note the rule is per failing command, not per compound status: for i in 1 2; do false; true; done notifies twice in all three shells even though the loop succeeds. lush already had that right, which is what pins the semantics.

2. Tested positions were reported (found while fixing #1)

A command whose failure is being tested is not an error -- the shell is asking whether it succeeds. lush reported three of the four tested forms:

lush before bash zsh
false && true T - -
! true T - -
if { false; }; then :; fi T - -
if false; then :; fi - - -

Tested-ness is inherited, which is why the fix is a depth rather than a flag: in true && false || true the inner && sits in the tested left position of the ||, so its final operand is not final overall.

3 and 4. The same root, in the opposite direction

  • A case arm ran its statements through no firing walker at all: case x in x) false; false;; esac notified once, where bash notifies twice.
  • A function body fired once for its aggregate result without asking what produced it, so with set -E a failing loop body notified three times. Without errtrace fire_err_trap suppresses in-function notifications, which is exactly why that one looked correct.

The fix

One rule at every site: report a failure where it originates, never where a status passes through, and never while testing.

  • g_err_tested_depth marks tested evaluation (conditions, the operand of !, non-final &&/|| operands) and gates every firing site.
  • err_trap_status_is_propagated names the constructs that report their own inner failures.
  • A subshell, a function call, select, time, a plain command and a pipeline are deliberately not listed -- their status originates where the walker sees it.

bash and zsh agree on every shape, and this is lush's own model of what the trap means, so it is mode-invariant and gates nothing.

Verification

  • 55-shape differential against bash: 55/55 identical, covering all four defect families, both set -E and set -e, pipelines and nesting.
  • tests/integration/test_err_trap_per_failure.c -- 41 checks, and 23 fail against the parent build with a control proving the binaries differ.
  • Full suite 193/193. ASan 65/65; the new test 1/1 under ASan.
  • errexit ordering preserved: the notification still precedes the abort, and a tested failure still does not abort (set -e; false || true reaches the next command).

…tatus (#729)

The ERR pseudo-signal notifies that a command FAILED. lush fired on any
non-zero status that reached a statement walker, which is a different thing,
and the gap showed in four shapes.

A compound command carrying a failure outward is the same event being
re-reported, so one failing command notified once per enclosing level:

    trap 'echo T' ERR; for i in 1; do for j in 1; do false; done; done
      before: T T T          bash, zsh: T

The count tracked nesting depth rather than failures. The rule is per failing
command, not per compound status -- a loop that SUCCEEDS still reports the
failures inside it (`for i in 1 2; do false; true; done` notifies twice in all
three shells), which lush already had right.

A command whose failure is being TESTED is not an error; the shell is asking
whether it succeeds. lush reported the tested position in three of its four
forms: a short-circuited `&& `, the operand of `!`, and any construct nested in
a condition all notified where no other shell does. Tested-ness is INHERITED,
which is why this is a depth rather than a flag: in `true && false || true` the
inner `&&` sits in the tested left position of the `||`, so its final operand
is not final overall.

The same root produced the opposite defect. A `case` arm ran its statements
through no firing walker at all, so two failing statements in an arm notified
ONCE where bash notifies twice. A function body had the mirror problem: it
fired once for the body's aggregate result without asking what produced it, so
with errtrace on a failing loop body notified three times. Without errtrace
fire_err_trap suppresses in-function notifications, which is why that one
looked correct.

The fix is one rule applied at every site: report a failure where it
originates, never where a status passes through, and never while testing.
g_err_tested_depth marks tested evaluation (conditions, the operand of `!`,
non-final `&&`/`||` operands) and gates every firing site;
err_trap_status_is_propagated names the constructs that report their own inner
failures. A subshell, a function call, `select`, `time`, a plain command and a
pipeline are deliberately not listed -- their status originates where the
walker sees it.

bash and zsh agree on every shape, and this is lush's own model of what the
trap means, so the fix is mode-invariant and gates nothing.

tests/integration/test_err_trap_per_failure.c: 41 checks -- nesting depth,
per-failure counting through a succeeding loop, case arms, every tested
position including the inherited one, pipelines, function calls with and
without errtrace, and errexit ordering. 23 fail against the parent build.
@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@berrym
berrym merged commit a6e6f33 into master Aug 16, 2026
7 checks passed
@berrym
berrym deleted the fix/729-err-trap-per-failure branch August 16, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ERR trap fires once per enclosing compound, not once per failed command

1 participant