diff --git a/src/utils/argsUtil.ts b/src/utils/argsUtil.ts index 5dd442ca3f..82cefb1f96 100644 --- a/src/utils/argsUtil.ts +++ b/src/utils/argsUtil.ts @@ -1,5 +1,7 @@ type ParseError = string +const escapeAtEndError = "args string has escape character (\\) at the end of the string, with nothing to escape."; + /** * Parses an argument string with shell-like semantics into a list of arguments. * Returns an error only if the argument string is malformed. @@ -7,60 +9,100 @@ type ParseError = string * - Whitespace is treated as the word separator. * - Each word is treated as an argument to be passed to the invocation process. * - Single-quotes and double-quotes can be used to escape whitespace characters as literals. + * - A backslash followed by a quote can be used (\' or \") to escape quotes as literals. * - Null arguments ("" or '') are retained and passed as empty strings. * When a null argument appears as part of a non-null argument, the null argument is removed. * That is, the word -d'' or ''-d becomes -d after word splitting and null argument removal. * @param args The string containing arguments to be parsed. */ export function parseArgsString(args: string): string[] | ParseError { - let result: string[] = []; + let resOrErr: string[] = []; let word: string = ""; let bufferedWord: boolean = false; for (let i = 0; i < args.length;) { if (args[i] == "'") { let j = i + 1; - for (; j < args.length && args[j] != "'"; j++) { + let k = i + 1; + for (; k < args.length && args[k] != "'";) { + if (args[k] == '\\' + && k + 1 < args.length + && (args[k + 1] == "'" || args[k + 1] == '"')) { + bufferedWord = true; + // buffer everything up to this point + word += args.slice(j, k) + word += args.charAt(k + 1) + + j = k + 2 + k = k + 2 + } else { + k++ + } } - if (j >= args.length) { + if (k >= args.length) { return "args string has unmatched single quotes ('). starting index: " + i } bufferedWord = true; - word += args.slice(i + 1, j); - i = j + 1; + word += args.slice(j, k); + i = k + 1; } else if (args[i] == '"') { let j = i + 1; - for (; j < args.length && args[j] != '"'; j++) { + let k = i + 1; + for (; k < args.length && args[k] != '"';) { + if (args[k] == '\\' + && k + 1 < args.length + && (args[k + 1] == "'" || args[k + 1] == '"')) { + bufferedWord = true; + // buffer everything up to this point + word += args.slice(j, k) + word += args.charAt(k + 1) + + j = k + 2 + k = k + 2 + } else { + k++; + } } - if (j >= args.length) { - return "args string has unmatched double quotes (\"). starting index: " + i + if (k >= args.length) { + return 'args string has unmatched double quotes ("). starting index: ' + i } bufferedWord = true; - word += args.slice(i + 1, j); - i = j + 1; + word += args.slice(j, k); + i = k + 1; + } else if (args[i] == '\\' + && i + 1 < args.length + && (args[i + 1] == "'" || args[i + 1] == '"')) { + bufferedWord = true; + word += args.charAt(i + 1); + i = i + 2; } else if (args[i] != ' ') { let j = i + 1; for (; j < args.length - && args[j] != ' ' && args[j] != "'" && args[j] != '"'; j++) { + && args[j] != ' ' + && args[j] != "'" + && args[j] != '"' + && args[j] != '\\'; + j++) { } bufferedWord = true; word += args.slice(i, j); i = j; } else if (bufferedWord) { // also true that args[i] == ' ' - result.push(word); + resOrErr.push(word); word = ""; bufferedWord = false; + i++; } else { // args[i] == ' ' i++ } } if (bufferedWord) { - result.push(word); + resOrErr.push(word); } - return result; + return resOrErr; } diff --git a/test/unit/util.test.ts b/test/unit/util.test.ts index 51770e0238..223dbd1e2a 100644 --- a/test/unit/util.test.ts +++ b/test/unit/util.test.ts @@ -21,7 +21,7 @@ suite('util tests', () => { want: ['value 1', 'value 2', 'value3', 'value4'] }, { - test: 'InnerQuotes', + test: 'SingleSurroundingDoubleQuotes', args: [`' "text" '`], want: [' "text" '] }, @@ -30,6 +30,16 @@ suite('util tests', () => { args: [`" 'text' "`], want: [` 'text' `] }, + { + test: 'EscapedQuotes', + args: [`\\'a \\'b \\"c \\"d`], + want: [`'a`, `'b`, '"c', '"d'] + }, + { + test: 'EscapedQuotesInsideQuotes', + args: [`'a\\' b\\"' "\\'c \\"d"`], + want: [`a' b"`, `'c "d`] + }, { test: 'Null', args: [`''`, '""'], @@ -37,7 +47,7 @@ suite('util tests', () => { }, { test: 'Empty', - args: [' '], + args: ['', ' ', ' '], want: [] as string[] }, {