Skip to content

fix(expand): unwind the command list on fatal ${...} expansion errors - #657

Merged
brianjfox merged 2 commits into
mainfrom
fix/expansion-error-list-abort
Aug 13, 2026
Merged

fix(expand): unwind the command list on fatal ${...} expansion errors#657
brianjfox merged 2 commits into
mainfrom
fix/expansion-error-list-abort

Conversation

@brianjfox

Copy link
Copy Markdown
Owner

Summary

A fatal ${...} expansion error — bad substitution, bad array subscript, invalid indirection — unwinds the whole command list in bash (the DISCARD longjmp): the rest of the current input line is abandoned, through && chains, compound commands and function calls, and the reader continues at the next line. In POSIX mode the error is fatal to a non-interactive shell outright (exit 127). gnash reported the error and failed the command, but ran the rest of the list.

Closes #655

Changes

  • core/src/expand.cpp: a scope guard on expand_dollar's ${...} branch escalates any arith_error newly raised inside it to the existing arith_abort unwind (the mechanism fatal assignment errors already use), covering every error site in the branch at once. Containment falls out of existing machinery: subshells and pipeline elements fork; eval strings, funsubs, in-process command substitutions and trap bodies run through run_string, which already ends the unwind.
  • Here-document exception: bash only aborts the redirection for an expansion error in a heredoc body. The executor's heredoc containment now saves/restores arith_abort too — which also stops the $((...)) branch's abort from escaping a heredoc (cat <<E; echo A with a bad $((1+)) body now runs the echo A, as bash does).
  • Redirect targets: an expansion error in a redirection target now fails the redirection (and the command, status 1) instead of opening the partially-expanded filename.
  • Loops/select: a fatal expansion error in a while/until condition or a for/select word list is the construct's own status 1, not the last completed body's.
  • Invalid @ transform (separate commit): ${v@Z} / ${v@} are fatal like ${x?} — a non-interactive shell exits 127, not the DISCARD's status 1. This also stops ${arr[@]@Z} printing its diagnostic once per element.
  • tests/harness/run_diff.sh: 22 new differential cases covering the unwind, every containment boundary, the heredoc exception, and the loop/transform statuses (470 total).

Verification

  • 47-case probe battery vs bash 5.3 matches on stdout and exit status: all ${...} error classes, &&/; chains, functions, subshells, pipelines, cmdsubs/backticks, eval, traps, heredocs, redirect targets, assignments, loops, [[, background jobs, POSIX mode (127), and the non-fatal classes ((( )), let, ${x-}, ${v/%%/x}) staying non-fatal.
  • ctest 23/23; run_diff 470/470.
  • Full 83-suite scoreboard sweep with fresh oracles: no regressions — comsub-eof (5, bash's own bug) and the read timing flake are unchanged, and errors improves 126 → 125 vs the pre-change binary; the remaining 125 lines are pre-existing divergences (readonly-assignment abort family, ${$VAR}), to be filed separately.

A fatal ${...} expansion error -- bad substitution, bad array subscript,
invalid indirection -- unwinds the whole command list in bash (the DISCARD
longjmp): the rest of the current input line is abandoned, through &&
chains, compound commands and function calls, and the reader continues at
the next line.  In POSIX mode the error is fatal to a non-interactive
shell outright (exit 127).  gnash reported the error and failed the
command but ran the rest of the list.

Escalate any arith_error newly raised inside expand_dollar's ${...}
branch to the existing arith_abort unwind (a scope guard covers every
error site in the branch at once).  Containment falls out of the existing
machinery: subshells and pipeline elements fork, and eval strings, funsubs
and trap bodies run through run_string, which already ends the unwind.

A here-document body is bash's exception -- the error only aborts the
redirection -- so the executor's heredoc containment now saves and
restores arith_abort too, which also stops the $((...)) branch's abort
from escaping a heredoc.  A redirection target whose expansion fails now
fails the redirection (and the command, status 1) instead of opening the
partially-expanded name.  A fatal expansion error in a while/until
condition or a for/select word list is the construct's own status 1
rather than the last completed body's.

Closes #655
…h 127

bash treats an unknown or missing transform operator (${v@Z}, ${v@})
as fatal like ${x?}: a non-interactive shell exits with status 127
rather than the DISCARD unwind's status 1.  Route the invalid-transform
report through the same exiting/127 path as ${x?} instead of
arith_error.  This also stops the splat form ${arr[@]@z} from printing
the diagnostic once per element.
@brianjfox
brianjfox merged commit 3b943cc into main Aug 13, 2026
1 check passed
@brianjfox
brianjfox deleted the fix/expansion-error-list-abort branch August 13, 2026 22:03
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.

Expansion errors do not abort the remainder of the command list

1 participant