Skip to content

Commit 048f433

Browse files
committed
Use balanced two-pass parsing for ambiguous escaped quotes
A backslash before a closing quote is inherently ambiguous between an escaped quote ("say \"hello\" now") and a literal trailing path separator ("C:\Program Files\Repo\"). Local lookahead cannot distinguish them, so parse in two deterministic passes: pass 1 prefers escaped quotes and is accepted when all quoted regions close; if it ends with an open quote (the trailing-separator signature), pass 2 treats backslashes before the active closing quote as literal. The whitespace/EOI lookahead special case is removed as subsumed.
1 parent c6159de commit 048f433

2 files changed

Lines changed: 30 additions & 10 deletions

File tree

plugins/codex/scripts/lib/args.mjs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function parseArgs(argv, config = {}) {
7373
return { options, positionals };
7474
}
7575

76-
export function splitRawArgumentString(raw) {
76+
function parseRawArgumentString(raw, literalClosingQuoteBackslash) {
7777
const tokens = [];
7878
let current = "";
7979
let quote = null;
@@ -83,12 +83,7 @@ export function splitRawArgumentString(raw) {
8383

8484
if (character === "\\") {
8585
const nextCharacter = raw[index + 1];
86-
const characterAfterQuote = raw[index + 2];
87-
const closesQuotedRegion =
88-
quote !== null &&
89-
nextCharacter === quote &&
90-
(characterAfterQuote === undefined || /\s/.test(characterAfterQuote));
91-
if (closesQuotedRegion) {
86+
if (literalClosingQuoteBackslash && quote !== null && nextCharacter === quote) {
9287
current += character;
9388
continue;
9489
}
@@ -133,5 +128,14 @@ export function splitRawArgumentString(raw) {
133128
tokens.push(current);
134129
}
135130

136-
return tokens;
131+
return { tokens, openQuote: quote };
132+
}
133+
134+
export function splitRawArgumentString(raw) {
135+
const escapePreferred = parseRawArgumentString(raw, false);
136+
if (escapePreferred.openQuote === null) {
137+
return escapePreferred.tokens;
138+
}
139+
140+
return parseRawArgumentString(raw, true).tokens;
137141
}

tests/args.test.mjs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ test("splitRawArgumentString preserves a quoted Windows path with spaces", () =>
1818
});
1919

2020
test("splitRawArgumentString closes a quoted Windows path after a trailing backslash", () => {
21-
assert.deepEqual(splitRawArgumentString(String.raw`--cwd "C:\Program Files\Repo\" --write`), [
22-
"--cwd",
21+
assert.deepEqual(splitRawArgumentString(String.raw`"C:\Program Files\Repo\" --write`), [
2322
"C:\\Program Files\\Repo\\",
2423
"--write"
2524
]);
@@ -32,14 +31,31 @@ test("splitRawArgumentString closes a final quoted Windows path after a trailing
3231
]);
3332
});
3433

34+
test("splitRawArgumentString closes a trailing-backslash path before another quoted token", () => {
35+
assert.deepEqual(splitRawArgumentString(String.raw`"C:\path\" "second"`), ["C:\\path\\", "second"]);
36+
});
37+
3538
test("splitRawArgumentString accepts an escaped quote inside double quotes", () => {
3639
assert.deepEqual(splitRawArgumentString(String.raw`"say \"hello\""`), ['say "hello"']);
3740
});
3841

42+
test("splitRawArgumentString preserves escaped quotes at word boundaries", () => {
43+
assert.deepEqual(splitRawArgumentString(String.raw`"say \"hello\" now"`), ['say "hello" now']);
44+
});
45+
3946
test("splitRawArgumentString preserves an escaped quote before a non-boundary character", () => {
4047
assert.deepEqual(splitRawArgumentString(String.raw`"say \"hi\"..."`), ['say "hi"...']);
4148
});
4249

50+
test("splitRawArgumentString parses plain prompts with quoted phrases", () => {
51+
assert.deepEqual(splitRawArgumentString(String.raw`run "quoted phrase" and "another phrase"`), [
52+
"run",
53+
"quoted phrase",
54+
"and",
55+
"another phrase"
56+
]);
57+
});
58+
4359
test("splitRawArgumentString collapses an escaped backslash", () => {
4460
assert.deepEqual(splitRawArgumentString(String.raw`C:\\repo`), [String.raw`C:\repo`]);
4561
});

0 commit comments

Comments
 (0)