Repository navigation
fix(core): treat a word-initial # as a comment when splitting shell commands #11821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
b2b5077
53fd793
9a977d8
f3cff68
50459ae
84bd8ee
f8f5fef
9023b45
20d53ca
5cdf8ca
5eb2dbe
93fe54e
4d1eb20
7098e33
9895c2a
6492faf
93a4101
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -863,6 +863,51 @@ function isAsyncOperator(command: string, index: number): boolean { | |
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Characters that end a bash word, so a `#` after one of them starts a word and | ||
| * so a comment: the three default `IFS` whitespace characters plus the operators | ||
| * that separate commands. The start of the input is handled by | ||
| * {@link isCommentStart} itself. | ||
| * | ||
| * The whitespace half is an explicit list rather than `\s`. JavaScript's `\s` | ||
| * also matches `\r`, `\v`, `\f` and `\u00a0`, and bash treats none of those as | ||
| * word separators: `bash -xc "$(printf 'echo a\r#c ; echo DANGER')"` traces two | ||
| * commands, because the `#` is mid-word and literal while the `;` after it is a | ||
| * real boundary. Reading that `#` as a comment folded the line into one segment, | ||
| * so the command bash ran second never got its own rule check. | ||
| * | ||
| * Confirmed against bash for the operators: `echo a;#c` and `echo a&#c` each run | ||
| * one command, and `echo a|#c` is a syntax error because the `#` comments out | ||
| * the right-hand side of the pipe. Redirection operators and `(` are | ||
| * deliberately absent: bash starts a word there too, but leaving them literal | ||
| * only over-splits, which stays fail-closed, and no reported shape needs them. | ||
| */ | ||
| const COMMENT_WORD_BOUNDARIES = [' ', '\t', '\n', ';', '&', '|']; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified the mechanical half of this claim against the code at I am not fixing this in this PR, and I am not resolving this thread either, because the fix as described is wider than the PR: the flag has to be threaded through Leaving this open for a maintainer call rather than half-gating one of the two consumers.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not taking this one inside this PR — asking for a ruling instead, because the fix is the design question this repo already tracks rather than a local patch. The rule is documented as bash's own at The measurement is not in dispute: folding What a closeout pass cannot settle is which of the two designs #11882 should land: an explicit
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-verified at Witness, running the real The path is intact at this head: Why this pass does not fix it locally. Nothing but the command string reaches the splitter today (
One correction to my earlier reply: every call site is in #11882 is open (
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not fixed, and I am leaving this open — the divergence is real but the fix is the architectural one this very diff already documents and defers. The Making Baseline measured at 4d1eb20 so the next round does not have to re-derive it: your witness is unchanged by the two fixes I landed — One gap in my own evidence, stated rather than papered over: this round's oracle was GNU bash 4.2.46 on Linux. I did not verify the cmd.exe leg on a Windows host, so I am not disputing your trace — only declining to redesign the shell-awareness boundary in this PR.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verdict unchanged, no code this round — see my 2026-09-16T04:19:27Z reply on this thread rather than re-arguing it; nothing in it has moved. What this round changed: nothing that touches the comment predicate. 93a4101 is confined to the heredoc projection ( Still a maintainer call, still deferred to #11882 (OPEN, |
||
|
|
||
| /** | ||
| * Whether the `#` at `index` opens a comment rather than being a literal. | ||
| * | ||
| * bash only starts a comment at the start of a word, so `echo a#b` prints | ||
| * `a#b` while `echo a #b` prints `a`. The same word rule keeps the arithmetic | ||
| * base prefix in `echo $(( 8#17 ))` literal. | ||
| * | ||
| * The boundary check is escape-aware: a backslash-escaped space or operator | ||
| * does not end a word, so the `#` after `a\ ` or `a\;` is mid-word and | ||
| * literal — treating it as a comment would swallow the rest of the line and | ||
| * merge a real `;` boundary away, dropping the rule check the following | ||
| * command would have got. | ||
| */ | ||
| function isCommentStart(command: string, index: number): boolean { | ||
|
yiliang114 marked this conversation as resolved.
Resolved
|
||
| if (index === 0) { | ||
| return true; | ||
| } | ||
| const previous = command[index - 1]!; | ||
| return ( | ||
| COMMENT_WORD_BOUNDARIES.includes(previous) && | ||
|
yiliang114 marked this conversation as resolved.
Resolved
|
||
| precedingBackslashCount(command, index - 1) % 2 === 0 | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * One segment of a compound command, together with the operator that ended it. | ||
| */ | ||
|
|
@@ -919,6 +964,24 @@ export function splitCompoundCommandSegments( | |
| continue; | ||
| } | ||
|
|
||
| // A word-initial `#` opens a comment that runs to the end of the physical | ||
|
yiliang114 marked this conversation as resolved.
Resolved
|
||
| // line, so an operator inside it is not a boundary: bash runs | ||
| // `echo 'a' # comment ; echo B` as a single `echo` (#11815). | ||
| // | ||
| // This scan deliberately ignores `escaped`. A backslash does not continue a | ||
| // line inside a comment — bash really does run the `rm` in `echo hi # foo \` | ||
| // followed by a newline and `rm -rf /` — so honouring the escape would fold | ||
| // that second command into the comment's segment and cost it its own rule | ||
| // check. For the same reason the newline is left for the operator scan | ||
| // below, and stays a boundary. | ||
| if (ch === '#' && isCommentStart(command, i)) { | ||
|
yiliang114 marked this conversation as resolved.
Resolved
|
||
| while (i < command.length && command[i] !== '\n') { | ||
|
yiliang114 marked this conversation as resolved.
Resolved
|
||
| i++; | ||
| } | ||
| i--; // -1 because the loop will i++, landing back on the newline | ||
| continue; | ||
| } | ||
|
|
||
| if (ch === '(' && command[i + 1] === '(') { | ||
| arithmeticDepth++; | ||
| i++; | ||
|
|
@@ -971,6 +1034,7 @@ export function splitCompoundCommandSegments( | |
| * "git status && rm -rf /" → ["git status", "rm -rf /"] | ||
| * "ls -la | grep foo" → ["ls -la", "grep foo"] | ||
| * "echo 'a && b'" → ["echo 'a && b'"] (inside quotes) | ||
| * "echo 'a' # c ; echo B" → ["echo 'a' # c ; echo B"] (inside a comment) | ||
| * "a && b || c" → ["a", "b", "c"] | ||
| * "git status & rm -rf /" → ["git status", "rm -rf /"] (async operator) | ||
| * "build &> log.txt" → ["build &> log.txt"] (redirection, not async) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.