-
Notifications
You must be signed in to change notification settings - Fork 10
fix: handle combined quotation shell arguments like Bash #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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)) { | ||||||||||||||||
| // 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
|
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| i++; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Don't forget the last token | ||||||||||||||||
| if (hasToken) { | ||||||||||||||||
| myArray.push(current); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
|
||||||||||||||||
| if (inQuote) { | |
| throw new Error( | |
| `Unclosed quote (${inQuote}) found in input string` | |
| ); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,7 +29,7 @@ describe("Process ", function () { | |
| parseAndValidate( | ||
| value.replace(/"/g, "'"), | ||
| expectedWithSingleQuotes, | ||
| false | ||
| false, | ||
| ); | ||
| } | ||
| } | ||
|
|
@@ -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(); | ||
|
|
@@ -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(); | ||
| }); | ||
|
|
@@ -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
|
||
|
|
||
| it("unquoted text followed by quoted text with space", function (done) { | ||
| parseAndValidate('a" b"', ["a b"]); | ||
| done(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
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.