Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 42 additions & 27 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,57 @@ 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);
}
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<any>): 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)) {
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating and executing a regex on every character iteration is inefficient. Consider using a simple character comparison instead (e.g., char === ' ' || char === '\t' || char === '\n' || char === '\r') or move the regex outside the loop to avoid repeated regex compilation.

Suggested change
} else if (/\s/.test(char)) {
} else if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {

Copilot uses AI. Check for mistakes.
// 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;
Comment on lines +15 to +48
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using string concatenation with the += operator in a loop can be inefficient for large input strings because strings are immutable in JavaScript, causing potential O(n²) time complexity. Consider using an array to collect characters and join them at the end, or use a more efficient string building approach for better performance with large inputs.

Copilot uses AI. Check for mistakes.
}
}
i++;
}

// Don't forget the last token
if (hasToken) {
myArray.push(current);
}

Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser doesn't handle unclosed quotes, which could lead to unexpected behavior. When a quote is opened but never closed, the entire rest of the input string will be consumed as part of the current token, including any subsequent arguments. Consider adding validation to detect unclosed quotes and either throw an error or handle them gracefully according to the desired behavior.

Suggested change
if (inQuote) {
throw new Error(
`Unclosed quote (${inQuote}) found in input string`
);
}

Copilot uses AI. Check for mistakes.
return myArray;
}
41 changes: 28 additions & 13 deletions test/Index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("Process ", function () {
parseAndValidate(
value.replace(/"/g, "'"),
expectedWithSingleQuotes,
false
false,
);
}
}
Expand All @@ -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();
Expand Down Expand Up @@ -77,7 +82,7 @@ describe("Process ", function () {
parseAndValidate(
'-testing test -valid=true --quotes "test quotes"',
["-testing", "test", "-valid=true", "--quotes", "test quotes"],
true
true,
);
done();
});
Expand All @@ -86,57 +91,67 @@ describe("Process ", function () {
parseAndValidate(
'-testing test -valid=true --quotes ""',
["-testing", "test", "-valid=true", "--quotes", ""],
true
true,
);
done();
});

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();
});

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();
});

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();
});
Comment on lines +143 to +151
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the new test cases cover combined quotation segments, there's no test coverage for important edge cases that could cause issues with the new parser implementation: unclosed quotes (e.g., --foo="bar), escaped quotes within strings (if supported), consecutive whitespace handling, and empty input strings. Adding tests for these scenarios would help ensure the parser handles them correctly.

Copilot uses AI. Check for mistakes.

it("unquoted text followed by quoted text with space", function (done) {
parseAndValidate('a" b"', ["a b"]);
done();
});
});