Skip to content

fix(hook): stop four shapes from closing a live block - #151

Merged
edouard-claude merged 1 commit into
masterfrom
fix/command-position-shapes
Jul 30, 2026
Merged

fix(hook): stop four shapes from closing a live block#151
edouard-claude merged 1 commit into
masterfrom
fix/command-position-shapes

Conversation

@edouard-claude

Copy link
Copy Markdown
Owner

Closes the first four shapes of #138. The fifth (a trailing backslash line continuation) has a different cause and is left for its own PR.

The defect

The command-position approximation read a reserved word that is not a command as one, popped a block that was still open, and the body was then rewritten. A consumer of the block read snip's compacted output instead of the real one. All four corrupt identically on master.

Reproduced end to end against real bash, 60-line file behind filters/grep.yaml's cap of head n:50:

shape raw before after
case $s in done) 60 51 60
for f in @(data|done).txt 60 51 60
time -p for i in 1 60 51 60
a)# done marker 60 51 60

The fixes

  • Case-arm patterns. blockScope tracks the arm state of a case: entered at the in that ends the subject, re-entered at each ;;, left at the arm's ). Words read in that state are patterns, so done) no longer pops the case. esac is the one exception, because bash rejects a bare esac as a pattern; without it the scope would stay open and everything after it would lose filtering. ;; is reported by the segmenter, since ';' is a group boundary and advance never sees one.
  • Extglob alternatives. Inside a paren that is not a subshell, '|' is pattern alternation and no longer restores command position.
  • time options. -p and -- keep command position, so the opener being timed is still seen. Bare time is unchanged.
  • )#. A '#' flush against an operator ')' opens a comment. advance decides this where the kind of the paren being closed is known, which distinguishes it from $(ls)#tail where the ')' ends a command substitution and the '#' is still inside the word. The segmenter's own isWordStart is deliberately untouched: adding '{' there would read ${#var} as a comment.

The file-level comment listing these four as known limitations is updated in the same commit.

Tests

TestRewriteCommandPosition covers the four shapes, the ;; re-arm on a later arm, and five non-regressions: $(ls)#tail, ${#f}, a subshell )#, bare time, and filtering resuming after esac. Every case fails on master.

make test-race, make verify (55/55) and golangci-lint are clean.

The command-position approximation read a reserved word that is not a
command as one, popped a block that was still open, and the body was
then rewritten: a consumer of the block read snip's compacted output
instead of the real one. All four corrupt identically on master.

- A case-arm pattern is a pattern, never a command. blockScope now
  tracks the arm state of a case, entered at `in` and re-entered at each
  `;;`, so `case $s in done)` no longer pops the case. `esac` still
  closes it, since bash rejects a bare `esac` as a pattern.
- Inside a paren that is not a subshell, '|' is pattern alternation, so
  it no longer restores command position: `for f in @(data|done).txt`
  keeps its loop open.
- `time` keeps command position across its own `-p` and `--` options, so
  the opener it times is still seen.
- A '#' flush against an operator ')' opens a comment, which isWordStart
  cannot tell from the ')' of a command substitution. advance decides it
  where the kind of the paren is known, leaving the segmenter's own
  isWordStart untouched (a '{' there would break ${#var}).

Verified end to end against real bash with a 60-line file behind
filters/grep.yaml's cap of 50: all four now return 60 where they
returned 51.

Refs #138
@edouard-claude
edouard-claude merged commit 50493fc into master Jul 30, 2026
3 checks passed
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