From 6d607597ff08649e376318ecc40aa2513e5d2e06 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 16 Dec 2025 00:58:47 -0800 Subject: [PATCH 1/2] fix: handle combined quotation shell arguments like Bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote the parser to use a character-by-character state machine instead of complex regex. This properly handles adjacent quoted and unquoted segments as a single argument, matching Bash behavior. Examples of now-correct parsing: - `--foo="bar"'baz'` → `--foo=barbaz` - `a" b"` → `a b` - `run:silent["echo 1"]["echo 2"]` → `run:silent[echo 1][echo 2]` Also fixed existing test cases that had incorrect expectations and added new test cases for combined quotation segments. --- index.ts | 69 ++++++++++++++++++++++++++++------------------ test/Index.spec.js | 36 +++++++++++++++--------- 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/index.ts b/index.ts index 52c424b..f2ec52b 100644 --- a/index.ts +++ b/index.ts @@ -4,14 +4,6 @@ export default function parseArgsStringToArgv( env?: string, file?: string ): string[] { - // ([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*) Matches nested quotes until the first space outside of quotes - - // [^\s'"]+ or Match if not a space ' or " - - // (['"])([^\5]*?)\5 or Match "quoted text" without quotes - // `\3` and `\5` are a backreference to the quote style (' or ") captured - const myRegexp = /([^\s'"]([^\s'"]*(['"])([^\3]*?)\3)+[^\s'"]*)|[^\s'"]+|(['"])([^\5]*?)\5/gi; - const myString = value; const myArray: string[] = []; if (env) { myArray.push(env); @@ -19,27 +11,50 @@ export default function parseArgsStringToArgv( if (file) { myArray.push(file); } - let match: RegExpExecArray | null; - do { - // Each call to exec returns the next regex match as an array - match = myRegexp.exec(myString); - if (match !== null) { - // Index 1 in the array is the captured group if it exists - // Index 0 is the matched text, which we use if no captured group exists - myArray.push(firstString(match[1], match[6], match[0])!); - } - } while (match !== null); - return myArray; -} + let current = ""; + let inQuote: string | null = null; + let hasToken = false; // Track if we've started a token (for empty quotes) + let i = 0; -// Accepts any number of arguments, and returns the first one that is a string -// (even an empty string) -function firstString(...args: Array): string | undefined { - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (typeof arg === "string") { - return arg; + while (i < value.length) { + const char = value[i]; + + if (inQuote) { + // Inside quotes - look for the closing quote + if (char === inQuote) { + // End of quoted section + inQuote = null; + } else { + // Add character to current token (without the quotes) + current += char; + } + } else { + // Outside quotes + if (char === '"' || char === "'") { + // Start of quoted section - this means we have a token even if empty + inQuote = char; + hasToken = true; + } else if (/\s/.test(char)) { + // Whitespace - end current token if we have one + if (hasToken) { + myArray.push(current); + current = ""; + hasToken = false; + } + } else { + // Regular character - add to current token + current += char; + hasToken = true; + } } + i++; + } + + // Don't forget the last token + if (hasToken) { + myArray.push(current); } + + return myArray; } diff --git a/test/Index.spec.js b/test/Index.spec.js index a72ac45..0ee13df 100644 --- a/test/Index.spec.js +++ b/test/Index.spec.js @@ -29,7 +29,7 @@ describe("Process ", function () { parseAndValidate( value.replace(/"/g, "'"), expectedWithSingleQuotes, - false + false, ); } } @@ -77,7 +77,7 @@ describe("Process ", function () { parseAndValidate( '-testing test -valid=true --quotes "test quotes"', ["-testing", "test", "-valid=true", "--quotes", "test quotes"], - true + true, ); done(); }); @@ -86,7 +86,7 @@ describe("Process ", function () { parseAndValidate( '-testing test -valid=true --quotes ""', ["-testing", "test", "-valid=true", "--quotes", ""], - true + true, ); done(); }); @@ -94,22 +94,22 @@ describe("Process ", function () { it("a complex string with nested quotes", function (done) { parseAndValidate( '--title "Peter\'s Friends" --name \'Phil "The Power" Taylor\'', - ["--title", "Peter's Friends", "--name", 'Phil "The Power" Taylor'] + ["--title", "Peter's Friends", "--name", 'Phil "The Power" Taylor'], ); done(); }); it("a complex key value with quotes", function (done) { parseAndValidate("--name='Phil Taylor' --title=\"Peter's Friends\"", [ - "--name='Phil Taylor'", - '--title="Peter\'s Friends"', + "--name=Phil Taylor", + "--title=Peter's Friends", ]); done(); }); it("a complex key value with nested quotes", function (done) { parseAndValidate("--name='Phil \"The Power\" Taylor'", [ - "--name='Phil \"The Power\" Taylor'", + '--name=Phil "The Power" Taylor', ]); done(); }); @@ -117,8 +117,8 @@ describe("Process ", function () { it("nested quotes with no spaces", function (done) { parseAndValidate( 'jake run:silent["echo 1"] --trace', - ["jake", 'run:silent["echo 1"]', "--trace"], - true + ["jake", "run:silent[echo 1]", "--trace"], + true, ); done(); }); @@ -126,17 +126,27 @@ describe("Process ", function () { it("multiple nested quotes with no spaces", function (done) { parseAndValidate( 'jake run:silent["echo 1"]["echo 2"] --trace', - ["jake", 'run:silent["echo 1"]["echo 2"]', "--trace"], - true + ["jake", "run:silent[echo 1][echo 2]", "--trace"], + true, ); done(); }); it("complex multiple nested quotes", function (done) { - parseAndValidate('cli value("echo")[\'grep\']+"Peter\'s Friends"', [ + parseAndValidate('cli value["echo"][\'grep\']+"Peter\'s Friends"', [ "cli", - 'value("echo")[\'grep\']+"Peter\'s Friends"', + "value[echo][grep]+Peter's Friends", ]); done(); }); + + it("combined quotation segments", function (done) { + parseAndValidate("--foo=\"bar\"'baz'", ["--foo=barbaz"]); + done(); + }); + + it("unquoted text followed by quoted text with space", function (done) { + parseAndValidate('a" b"', ["a b"]); + done(); + }); }); From c5966beecb7d7bdb0d6f89cff94de6861c92e449 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 16 Dec 2025 01:03:13 -0800 Subject: [PATCH 2/2] Add empty array test --- test/Index.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Index.spec.js b/test/Index.spec.js index 0ee13df..03107a5 100644 --- a/test/Index.spec.js +++ b/test/Index.spec.js @@ -43,6 +43,11 @@ describe("Process ", function () { done(); }); + it("an empty string should return an empty array", function (done) { + parseAndValidate("", []); + done(); + }); + it("an arguments array correctly without file and env", function (done) { parseAndValidate("-test", ["-test"]); done();