POSIX recognizes an IO number only when the digit run is unquoted, but tokenize()'s redirection branch tested `cur` — the token's characters — and never `curMask`, their quoting. `"2"` and `2` were indistinguishable to it, so a correct `gsd_run query commit ... --files "2">out` had its value consumed as an IO number and scored as an unscoped invocation: the #2269 guard reddening on documentation that was right. The mask was already maintained in the same loop and thrown away on this branch. One conjunct reads it: '0' marks a bare character, so /^0+$/ asks precisely whether every character of the digit run was unquoted. The regression arms come in both directions. The quoted rows (glued, detached, single-quoted, and the partially-quoted 2"3" that a some-character-unquoted test would get wrong) must be scoped; the existing unquoted `--files 2>&1` row is the negative control and must stay unscoped, so an implementation that simply stopped consuming IO numbers altogether fails instead of passing. No live instance exists in any of the six scan roots, so this is latent rather than urgent — a false positive, never a silent miss. Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
@@ -1573,6 +1573,12 @@ describe('workflow call sites declare --files (#2269)', () => {
|
||||
// vouch as a value.
|
||||
'gsd_run query commit "docs: revert" --files >/dev/null 2>&1 || true',
|
||||
'gsd_run query commit "docs: plan" --files > out.md',
|
||||
// #4276 NEGATIVE CONTROL. Here the 2 is genuinely an IO number: the
|
||||
// shell consumes `2>&1` whole and --files reaches argv with no value.
|
||||
// This row is what stops the quoted-digit fix below from over-
|
||||
// correcting into "stop consuming IO numbers" — an implementation that
|
||||
// simply dropped the redirection branch would satisfy the scoped rows
|
||||
// and fail here.
|
||||
'gsd_run query commit "docs: plan" --files 2>&1',
|
||||
// An unquoted # begins a comment: everything after it, --files included,
|
||||
// never reaches argv.
|
||||
@@ -1612,6 +1618,20 @@ describe('workflow call sites declare --files (#2269)', () => {
|
||||
'gsd_run query commit "docs: ship phase 4 — PR #42 [ci skip]" --files .planning/STATE.md',
|
||||
// Mid-word # is literal too, as in the shell.
|
||||
'gsd_run query commit docs:PR#42 --files .planning/STATE.md',
|
||||
// #4276: a QUOTED digit run is not an IO number. POSIX recognizes one
|
||||
// only when the digits are bare, so the shell passes `"2"` as an
|
||||
// ordinary argument and this invocation IS scoped — on a file named
|
||||
// `2`, which is legal. Before the fix the tokenizer read the token's
|
||||
// characters without its quoting, ate the value as an IO number, and
|
||||
// reported a correct line as unscoped.
|
||||
'gsd_run query commit "docs: plan" --files "2">out',
|
||||
// Detached from the operator, and single-quoted, so neither the gluing
|
||||
// nor the quote style is what carries the fix.
|
||||
'gsd_run query commit "docs: plan" --files \'2\' > out',
|
||||
// Partially quoted: `2"3"` is not a bare digit run either, and a mask
|
||||
// test that asked "any character unquoted?" instead of "every
|
||||
// character unquoted?" would get this one wrong.
|
||||
'gsd_run query commit "docs: plan" --files 2"3">out',
|
||||
];
|
||||
for (const line of scoped) {
|
||||
assert.ok(invocationCandidates(line).length > 0, `should be an invocation: ${line}`);
|
||||
|
||||
@@ -146,8 +146,17 @@ const tokenize = (str) => {
|
||||
// Redirections: the operator and its target are consumed by the shell,
|
||||
// not passed as arguments. A glued all-digit word is an IO number
|
||||
// (`2>&1`) and belongs to the redirection, not to argv.
|
||||
//
|
||||
// #4276: POSIX recognizes an IO number only when the digit run is
|
||||
// UNQUOTED, so the test has to read the quoting and not just the
|
||||
// characters. `cur` has already lost that distinction — `"2"` and `2`
|
||||
// reach here identically — and `curMask` is where it survives ('0' marks
|
||||
// a bare character). Without the mask conjunct a correct
|
||||
// `--files "2">out` had its value eaten as an IO number and scored as an
|
||||
// unscoped invocation: a false positive on documentation that was right.
|
||||
if (ch === '>' || ch === '<') {
|
||||
if (started && /^[0-9]+$/.test(cur)) { cur = ''; curMask = ''; started = false; } else { flush(i); }
|
||||
const bareDigitRun = /^[0-9]+$/.test(cur) && /^0+$/.test(curMask);
|
||||
if (started && bareDigitRun) { cur = ''; curMask = ''; started = false; } else { flush(i); }
|
||||
let j = i + 1;
|
||||
if (str[j] === ch) j += 1; // >> / <<
|
||||
if (str[j] === '&') j += 1; // >& (2>&1)
|
||||
|
||||
Reference in New Issue
Block a user