Skip to content

fix: attach trailing redirects to compound commands - #20

Draft
378-kaiabot[bot] wants to merge 2 commits into
fix/heredoc-body-queuefrom
fix/compound-command-redirects
Draft

fix: attach trailing redirects to compound commands#20
378-kaiabot[bot] wants to merge 2 commits into
fix/heredoc-body-queuefrom
fix/compound-command-redirects

Conversation

@378-kaiabot

@378-kaiabot 378-kaiabot Bot commented Sep 2, 2026

Copy link
Copy Markdown

Note

Automated pull request by Pi, model aperture/synthetic/hf:zai-org/GLM-5.3-Flash. Implemented and validated empirically against aliou/sh on branch fix/heredoc-body-queue. See the local run log for the full investigation.

Summary

Third layer of the heredoc/redirect work, stacked on #18. PR #18's review surfaced a worse-than-throwing edge: redirects written after a compound command's closing keyword (done, fi, }, ), esac, ]]) were silently captured into a phantom word-less SimpleCommand statement instead of attaching to the compound node. Injury, error, and now insult: while read x; do echo $x; done < in | sort parsed < in as a redirect-only statement at the left of a new pipeline — the loop lost its stdin and the pipeline shape broke, with no error signal.

Bash and mvdan/sh attach these redirects to the compound node itself.

Changes

  • src/ast.ts — optional redirects?: Redirect[] on Subshell, Block, IfClause, WhileClause, ForClause, SelectClause, FunctionDecl, CaseClause, TestClause, CoprocClause, CStyleLoop. Field stays absent when there are no redirects, so every existing fixture is untouched.
  • src/parser/compound-redirects.test.ts — commit 1/2: fifteen failing tests (loops, if, block, subshell, case, select, function, [[ ]], coproc, heredoc-on-compound, multiple redirects, and the pipeline case). Do not modify these to make the fix pass.
  • src/parser/parser.ts — commit 2/2: a shared parseTrailingRedirects runs after each compound's closing keyword and attaches redirects to that node. Two subtleties it handles: redirects after a function's block bind to the FunctionDecl (and coproc bodies to the CoprocClause), so capture is suppressed in the body scope and re-enabled for nested statement lists; and heredoc openers flow through the existing pendingHeredocs queue from fix: queue heredoc bodies per command line and drain in opener order #18, so done <<IN bodies still attach in order. time { a; } > out intentionally keeps binding to the timed block — TimeClause has no redirects field.
  • src/test-helpers/ast-builders.tscStyleLoop now routes clause strings through the real arithmetic parser (the ArithLit placeholders never matched parser output).
  • .changeset/fuzzy-hounds-tease.md — patch changeset.

Verification

  • pnpm typecheck
  • pnpm lint
  • pnpm test ✅ (305 passed; the 15 new tests fail on fix: queue heredoc bodies per command line and drain in opener order #18's tip and pass here)
  • Reviewer probes beyond the test file: nested loops inside function bodies, nested subshells, &&/background ordering, heredoc on a function, elif chains (redirect attaches once, to the outer if), unnamed coproc, dup-fd redirects (>&2), redirect-then-comment, position extents — all verified empirically.

Known limit (pre-existing, unchanged): time false > t2 still binds to the timed command, since TimeClause carries no redirects.

Stacked on #18


Run host: sh-d9e31b · Session: 01a062c1-c9e1-71cb-b132-aafe96837313 · Model: aperture/synthetic/hf:zai-org/GLM-5.3-Flash (validation), aperture/neuralwatt/kimi-k3 (implementation)

Redirects following fi/done/}/)/esac currently land on a phantom
word-less SimpleCommand statement instead of the compound node itself;
bash and mvdan/sh attach them to the compound (WhileClause, IfClause,
Block, Subshell, ...). Adds optional redirects fields to the compound
AST node types as the contract for the fix, plus failing tests
asserting the correct shapes, including heredoc and pipeline
interactions.
Redirects written after a compound command's closing keyword
(done, fi, }, ), esac, ]]) used to be captured into a phantom
word-less SimpleCommand statement, silently disconnecting them from
the loop/if/block they apply to (e.g. 'while read x; do ...; done
< files | sort' produced a redirect-only statement as the pipeline
left instead of redirecting the loop).

A shared parseTrailingRedirects step now consumes redirects following
each closer's compound node, mirroring bash and mvdan/sh. Function
definitions and coproc bodies cause redirects after their blocks to
bind to the FunctionDecl/CoprocClause instead, so capture is
suppressed in the body scope and re-enabled for nested statement
lists. Also routes the cStyleLoop test builder through the real
arithmetic parser so fixtures match parser output.
@378-kaiabot 378-kaiabot Bot changed the title fix/compound command redirects fix: attach trailing redirects to compound commands Sep 2, 2026
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.

1 participant