fix(arg-parser): a quoted value closes at a quote that ends a word, also after whitespace
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ce98f62e82
commit
8c52c3c32e
2 changed files with 44 additions and 11 deletions
|
|
@ -137,15 +137,16 @@ const isSpace = (c) => /\s/.test(c);
|
|||
* Split argument text into words. Words are separated by whitespace. A word
|
||||
* that starts with `'` or `"` opens a quote. Its closing quote is the next
|
||||
* same-kind quote that ends a word (followed by whitespace or the end of the
|
||||
* text) and does not start one; a same-kind quote inside a word is skipped.
|
||||
* The search stops at a same-kind quote that starts a word (one preceded by
|
||||
* whitespace): then there is no pair. A pair groups the words between the
|
||||
* quotes into one value and is removed. A quote with no partner is a literal
|
||||
* character (`user's` stays `user's`). Examples: `'Fix the user's login'` is
|
||||
* one value, `Fix the user's login`; `"my dir"/x --since "HEAD~1"` gives the
|
||||
* literal words `"my` and `dir"/x`, then `--since` and `HEAD~1`, so a quoted
|
||||
* value never swallows a later flag. Nothing is expanded: the function only
|
||||
* slices the string.
|
||||
* text), whatever stands before it: a letter, whitespace or a line break. A
|
||||
* same-kind quote that starts a word (whitespace before it, a non-whitespace
|
||||
* character after it) stops the search: then there is no pair. Any other
|
||||
* same-kind quote (inside a word) is skipped. A pair groups the words between
|
||||
* the quotes into one value and is removed. A quote with no partner is a
|
||||
* literal character (`user's` stays `user's`). Examples: `"my dir "` is one
|
||||
* value, `my dir `; `'Fix the user's login'` is one value,
|
||||
* `Fix the user's login`; `"my dir"/x --since "HEAD~1"` gives the literal
|
||||
* words `"my` and `dir"/x`, then `--since` and `HEAD~1`. Nothing is expanded:
|
||||
* the function only slices the string.
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {Array<{ value: string, start: number, end: number }>}
|
||||
|
|
@ -163,8 +164,11 @@ export function tokenize(text) {
|
|||
const q = text[i];
|
||||
if (q === '"' || q === "'") {
|
||||
let j = text.indexOf(q, i + 1);
|
||||
while (j !== -1 && !isSpace(text[j - 1]) && j + 1 < n && !isSpace(text[j + 1])) j = text.indexOf(q, j + 1);
|
||||
if (j !== -1 && !isSpace(text[j - 1])) {
|
||||
while (j !== -1 && j + 1 < n && !isSpace(text[j + 1])) {
|
||||
if (isSpace(text[j - 1])) { j = -1; break; }
|
||||
j = text.indexOf(q, j + 1);
|
||||
}
|
||||
if (j !== -1) {
|
||||
out.push({ value: text.slice(i + 1, j), start, end: j + 1 });
|
||||
i = j + 1;
|
||||
continue;
|
||||
|
|
|
|||
29
tests/lib/arg-parser-closing-quote.test.mjs
Normal file
29
tests/lib/arg-parser-closing-quote.test.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// [CQW] Fix round 4.5b, part 3 (decision 89).
|
||||
// Defect: `tokenize` refused a closing quote that had whitespace or a line
|
||||
// break before it, so `"multi line` + line break + `"` (the form the `--stdin`
|
||||
// heredoc produces), `--project "my dir "`, `"a "` and `" "` never paired and
|
||||
// their quotes stayed literal characters. A same-kind quote that ends a word
|
||||
// must close the value, whatever stands before it.
|
||||
// This file must load at ce98f62: it imports only `parseArgs` and `tokenize`,
|
||||
// which exist there, so its red is an assertion, not a crash.
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { parseArgs, tokenize } from '../../lib/parsers/arg-parser.mjs';
|
||||
|
||||
const values = (text) => tokenize(text).map((t) => t.value);
|
||||
|
||||
test('[CQW] a quoted value closes at a quote that ends a word, also after whitespace or a line break', () => {
|
||||
// Red at the start point: the closing quote has whitespace before it.
|
||||
assert.deepEqual(values('"multi line\n" --since HEAD'), ['multi line\n', '--since', 'HEAD']);
|
||||
const r = parseArgs('--project "my dir "', 'trekreview');
|
||||
assert.equal(r.flags['--project'], 'my dir ');
|
||||
assert.deepEqual(r.positional, []);
|
||||
assert.deepEqual(values('"a "'), ['a ']);
|
||||
assert.deepEqual(values('" "'), [' ']);
|
||||
|
||||
// Control: a quote that starts a word stops the search; a quote inside a
|
||||
// word is skipped; a quote that ends a word after a letter still closes.
|
||||
assert.equal(parseArgs('--project "my dir"/x --since "HEAD~1"', 'trekreview').flags['--since'], 'HEAD~1');
|
||||
assert.deepEqual(values("'Fix the user's login'"), ["Fix the user's login"]);
|
||||
assert.deepEqual(values("'the '90s were great'"), ["'the", '90s were great']);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue