From 36db849ff1f4dfd3aead5c23e23c59e9c2bcfa85 Mon Sep 17 00:00:00 2001 From: RevanProdigalKnight Date: Wed, 4 May 2022 00:46:15 -0600 Subject: [PATCH 1/6] Upgrade terminal ParseCommand function Adds ability to parse arbitrarily-nested strings into single arguments (see comments on lines 48, 64, 76) Adds ability to parse additional number formats, e.g.: hexadecimal (0x0A), octal (018), and binary (0b1101) Combines arguments separated with escaped spaces into single string arguments, e.g.: "echo Hello\ World!" -> ["echo", "Hello World!"] --- src/Terminal/Parser.ts | 167 ++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 95 deletions(-) diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts index fc38c1ba3..c99d15c39 100644 --- a/src/Terminal/Parser.ts +++ b/src/Terminal/Parser.ts @@ -1,12 +1,21 @@ import { KEY } from "../utils/helpers/keyCodes"; import { substituteAliases } from "../Alias"; -// Helper function that checks if an argument (which is a string) is a valid number -function isNumber(str: string): boolean { - if (typeof str != "string") { - return false; - } // Only process strings - return !isNaN(str as unknown as number) && !isNaN(parseFloat(str)); +// Helper function to parse individual arguments into number/boolean/string as appropriate +function parseArg(arg) { + // Handles all numbers including hexadecimal, octal, and binary representations, returning NaN on an unparseable string + const asNumber = Number(arg); + if (!isNaN(asNumber)) { + return asNumber; + } + + if (arg === 'true' || arg === 'false') { + return arg === 'true'; + } + + // Strip quotation marks from strings that begin/end with the same mark + return arg.replace(/^"(.*?)"$/g, '$1').replace(/^'(.*?)'$/g, '$1'); } + export function ParseCommands(commands: string): string[] { // Sanitize input commands = commands.trim(); @@ -33,99 +42,67 @@ export function ParseCommands(commands: string): string[] { } export function ParseCommand(command: string): (string | number | boolean)[] { - // This will be used to keep track of whether we're in a quote. This is for situations - // like the alias command: - // alias run="run NUKE.exe" - // We want the run="run NUKE.exe" to be parsed as a single command, so this flag - // will keep track of whether we have a quote in - let inQuote = ``; - - // Returns an array with the command and its arguments in each index - // Properly handles quotation marks (e.g. `run foo.script "the sun"` will return [run, foo.script, the sun]) + let idx = 0; const args = []; - let start = 0, - i = 0; - let prevChar = ""; // Previous character - while (i < command.length) { - let escaped = false; // Check for escaped quotation marks - if (i >= 1) { - prevChar = command.charAt(i - 1); - if (prevChar === "\\") { - escaped = true; + + // Track depth of quoted strings, e.g.: "the're 'going away' rather 'quickly \"and awkwardly\"'" should be parsed as a single string + const quotes = []; + + let arg = ''; + while (idx < command.length) { + const c = command.charAt(idx); + + // If the current character is a backslash, add the next character verbatim to the argument + if (c === '\\') { + arg += command.charAt(++idx); + // If the current character is a single- or double-quote mark, add it to the current argument. + } else if (c === KEY.DOUBLE_QUOTE || c === KEY.QUOTE) { + arg += c; + const quote = quotes[quotes.length - 1]; + const prev = command.charAt(idx - 1); + const next = command.charAt(idx + 1); + // If the previous character is a space or an equal sign this is a valid start to a new string. + // If we're already in a quoted string, push onto the stack of string starts to track depth. + if ( + c !== quote && + ( + prev === KEY.SPACE || + prev === KEY.EQUAL || + (c === KEY.DOUBLE_QUOTE && prev === KEY.QUOTE) || + (c === KEY.QUOTE && prev === KEY.DOUBLE_QUOTE) + ) + ) { + quotes.push(c); + // If the next character is a space and the current character is the same as the previously used + // quotation mark, this is a valid end to a string. Pop off the depth tracker. + } else if ( + c === quote && + ( + next === KEY.SPACE || + (c === KEY.DOUBLE_QUOTE && next === KEY.QUOTE) || + (c === KEY.QUOTE && next === KEY.DOUBLE_QUOTE) + ) + ) { + quotes.pop(); } - } - - const c = command.charAt(i); - if (c === KEY.DOUBLE_QUOTE) { - // Double quotes - if (!escaped && prevChar === KEY.SPACE) { - const endQuote = command.indexOf(KEY.DOUBLE_QUOTE, i + 1); - if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) { - args.push(command.substr(i + 1, endQuote - i - 1)); - if (endQuote === command.length - 1) { - start = i = endQuote + 1; - } else { - start = i = endQuote + 2; // Skip the space - } - continue; - } - } else if (inQuote === ``) { - inQuote = KEY.DOUBLE_QUOTE; - } else if (inQuote === KEY.DOUBLE_QUOTE) { - inQuote = ``; - } - } else if (c === KEY.QUOTE) { - // Single quotes, same thing as above - if (!escaped && prevChar === KEY.SPACE) { - const endQuote = command.indexOf(KEY.QUOTE, i + 1); - if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === KEY.SPACE)) { - args.push(command.substr(i + 1, endQuote - i - 1)); - if (endQuote === command.length - 1) { - start = i = endQuote + 1; - } else { - start = i = endQuote + 2; // Skip the space - } - continue; - } - } else if (inQuote === ``) { - inQuote = KEY.QUOTE; - } else if (inQuote === KEY.QUOTE) { - inQuote = ``; - } - } else if (c === KEY.SPACE && inQuote === ``) { - const arg = command.substr(start, i - start); - - // If this is a number, convert it from a string to number - if (isNumber(arg)) { - args.push(parseFloat(arg)); - } else if (arg === "true") { - args.push(true); - } else if (arg === "false") { - args.push(false); - } else { - args.push(arg); - } - - start = i + 1; - } - ++i; - } - - // Add the last argument - if (start !== i) { - const arg = command.substr(start, i - start); - - // If this is a number, convert it from string to number - if (isNumber(arg)) { - args.push(parseFloat(arg)); - } else if (arg === "true") { - args.push(true); - } else if (arg === "false") { - args.push(false); + // If the current character is a space and we are not inside a string, parse the current argument + // and start a new one + } else if (c === KEY.SPACE && quotes.length === 0) { + args.push(parseArg(arg)); + + arg = ''; } else { - args.push(arg); + // Add the current character to the current argument + arg += c; } + + idx++; } - + + // Add the last arg (if any) + if (arg !== '') { + args.push(parseArg(arg)); + } + return args; } From df30771744e58f5d639a8c26bf1393b906b73426 Mon Sep 17 00:00:00 2001 From: RevanProdigalKnight Date: Wed, 4 May 2022 00:47:58 -0600 Subject: [PATCH 2/6] Add = Adds the equal sign (`=`) for use in `src/Terminal/Parser.ts` --- src/utils/helpers/keyCodes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/helpers/keyCodes.ts b/src/utils/helpers/keyCodes.ts index 29231dfec..91f44a657 100644 --- a/src/utils/helpers/keyCodes.ts +++ b/src/utils/helpers/keyCodes.ts @@ -25,6 +25,7 @@ export enum KEY { CLOSE_PARENTHESIS = ")", OPEN_BRACE = "{", CLOSE_BRACE = "}", + EQUAL = "=", PIPE = "|", DOT = ".", From 8d5f80de266ce12567146f29038aea7dabfdd34b Mon Sep 17 00:00:00 2001 From: RevanProdigalKnight Date: Wed, 4 May 2022 08:27:52 -0600 Subject: [PATCH 3/6] Fixes linter/build errors from editing last night in GitHub --- src/Terminal/Parser.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts index c99d15c39..696d7ec9e 100644 --- a/src/Terminal/Parser.ts +++ b/src/Terminal/Parser.ts @@ -1,17 +1,17 @@ import { KEY } from "../utils/helpers/keyCodes"; import { substituteAliases } from "../Alias"; // Helper function to parse individual arguments into number/boolean/string as appropriate -function parseArg(arg) { +function parseArg(arg: string): string | number | boolean { // Handles all numbers including hexadecimal, octal, and binary representations, returning NaN on an unparseable string const asNumber = Number(arg); if (!isNaN(asNumber)) { return asNumber; } - + if (arg === 'true' || arg === 'false') { return arg === 'true'; } - + // Strip quotation marks from strings that begin/end with the same mark return arg.replace(/^"(.*?)"$/g, '$1').replace(/^'(.*?)'$/g, '$1'); } @@ -44,14 +44,14 @@ export function ParseCommands(commands: string): string[] { export function ParseCommand(command: string): (string | number | boolean)[] { let idx = 0; const args = []; - + // Track depth of quoted strings, e.g.: "the're 'going away' rather 'quickly \"and awkwardly\"'" should be parsed as a single string - const quotes = []; - + const quotes: string[] = []; + let arg = ''; while (idx < command.length) { const c = command.charAt(idx); - + // If the current character is a backslash, add the next character verbatim to the argument if (c === '\\') { arg += command.charAt(++idx); @@ -89,20 +89,20 @@ export function ParseCommand(command: string): (string | number | boolean)[] { // and start a new one } else if (c === KEY.SPACE && quotes.length === 0) { args.push(parseArg(arg)); - + arg = ''; } else { // Add the current character to the current argument arg += c; } - + idx++; } - + // Add the last arg (if any) if (arg !== '') { - args.push(parseArg(arg)); + args.push(parseArg(arg)); } - + return args; } From 286ab64d67b76a88d45eb47ea284dcd95f9b2670 Mon Sep 17 00:00:00 2001 From: RevanProdigalKnight Date: Wed, 4 May 2022 08:32:55 -0600 Subject: [PATCH 4/6] Ran formatter --- src/Terminal/Parser.ts | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts index 696d7ec9e..47a5bff10 100644 --- a/src/Terminal/Parser.ts +++ b/src/Terminal/Parser.ts @@ -8,12 +8,12 @@ function parseArg(arg: string): string | number | boolean { return asNumber; } - if (arg === 'true' || arg === 'false') { - return arg === 'true'; + if (arg === "true" || arg === "false") { + return arg === "true"; } // Strip quotation marks from strings that begin/end with the same mark - return arg.replace(/^"(.*?)"$/g, '$1').replace(/^'(.*?)'$/g, '$1'); + return arg.replace(/^"(.*?)"$/g, "$1").replace(/^'(.*?)'$/g, "$1"); } export function ParseCommands(commands: string): string[] { @@ -48,14 +48,14 @@ export function ParseCommand(command: string): (string | number | boolean)[] { // Track depth of quoted strings, e.g.: "the're 'going away' rather 'quickly \"and awkwardly\"'" should be parsed as a single string const quotes: string[] = []; - let arg = ''; + let arg = ""; while (idx < command.length) { const c = command.charAt(idx); // If the current character is a backslash, add the next character verbatim to the argument - if (c === '\\') { + if (c === "\\") { arg += command.charAt(++idx); - // If the current character is a single- or double-quote mark, add it to the current argument. + // If the current character is a single- or double-quote mark, add it to the current argument. } else if (c === KEY.DOUBLE_QUOTE || c === KEY.QUOTE) { arg += c; const quote = quotes[quotes.length - 1]; @@ -65,32 +65,28 @@ export function ParseCommand(command: string): (string | number | boolean)[] { // If we're already in a quoted string, push onto the stack of string starts to track depth. if ( c !== quote && - ( - prev === KEY.SPACE || + (prev === KEY.SPACE || prev === KEY.EQUAL || (c === KEY.DOUBLE_QUOTE && prev === KEY.QUOTE) || - (c === KEY.QUOTE && prev === KEY.DOUBLE_QUOTE) - ) + (c === KEY.QUOTE && prev === KEY.DOUBLE_QUOTE)) ) { quotes.push(c); - // If the next character is a space and the current character is the same as the previously used - // quotation mark, this is a valid end to a string. Pop off the depth tracker. + // If the next character is a space and the current character is the same as the previously used + // quotation mark, this is a valid end to a string. Pop off the depth tracker. } else if ( c === quote && - ( - next === KEY.SPACE || + (next === KEY.SPACE || (c === KEY.DOUBLE_QUOTE && next === KEY.QUOTE) || - (c === KEY.QUOTE && next === KEY.DOUBLE_QUOTE) - ) + (c === KEY.QUOTE && next === KEY.DOUBLE_QUOTE)) ) { quotes.pop(); } - // If the current character is a space and we are not inside a string, parse the current argument - // and start a new one + // If the current character is a space and we are not inside a string, parse the current argument + // and start a new one } else if (c === KEY.SPACE && quotes.length === 0) { args.push(parseArg(arg)); - arg = ''; + arg = ""; } else { // Add the current character to the current argument arg += c; @@ -100,7 +96,7 @@ export function ParseCommand(command: string): (string | number | boolean)[] { } // Add the last arg (if any) - if (arg !== '') { + if (arg !== "") { args.push(parseArg(arg)); } From 7a6809f66cade3296b3d555e5b7becbeb9600197 Mon Sep 17 00:00:00 2001 From: RevanProdigalKnight Date: Fri, 13 May 2022 21:39:20 -0600 Subject: [PATCH 5/6] Simplify quoted string argument parsing --- src/Terminal/Parser.ts | 41 +++++++++++++---------------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts index 47a5bff10..9e44f250c 100644 --- a/src/Terminal/Parser.ts +++ b/src/Terminal/Parser.ts @@ -12,8 +12,7 @@ function parseArg(arg: string): string | number | boolean { return arg === "true"; } - // Strip quotation marks from strings that begin/end with the same mark - return arg.replace(/^"(.*?)"$/g, "$1").replace(/^'(.*?)'$/g, "$1"); + return arg; } export function ParseCommands(commands: string): string[] { @@ -45,8 +44,7 @@ export function ParseCommand(command: string): (string | number | boolean)[] { let idx = 0; const args = []; - // Track depth of quoted strings, e.g.: "the're 'going away' rather 'quickly \"and awkwardly\"'" should be parsed as a single string - const quotes: string[] = []; + let lastQuote = ""; let arg = ""; while (idx < command.length) { @@ -57,33 +55,20 @@ export function ParseCommand(command: string): (string | number | boolean)[] { arg += command.charAt(++idx); // If the current character is a single- or double-quote mark, add it to the current argument. } else if (c === KEY.DOUBLE_QUOTE || c === KEY.QUOTE) { - arg += c; - const quote = quotes[quotes.length - 1]; - const prev = command.charAt(idx - 1); - const next = command.charAt(idx + 1); - // If the previous character is a space or an equal sign this is a valid start to a new string. - // If we're already in a quoted string, push onto the stack of string starts to track depth. - if ( - c !== quote && - (prev === KEY.SPACE || - prev === KEY.EQUAL || - (c === KEY.DOUBLE_QUOTE && prev === KEY.QUOTE) || - (c === KEY.QUOTE && prev === KEY.DOUBLE_QUOTE)) - ) { - quotes.push(c); - // If the next character is a space and the current character is the same as the previously used - // quotation mark, this is a valid end to a string. Pop off the depth tracker. - } else if ( - c === quote && - (next === KEY.SPACE || - (c === KEY.DOUBLE_QUOTE && next === KEY.QUOTE) || - (c === KEY.QUOTE && next === KEY.DOUBLE_QUOTE)) - ) { - quotes.pop(); + // If we're currently in a quoted string argument and this quote mark is the same as the beginning, + // the string is done + if (lastQuote !== "" && c === lastQuote) { + lastQuote = ""; + // Otherwise if we're not in a string argument, we've begun one + } else if (lastQuote === "") { + lastQuote = c; + // Otherwise if we're in a string argument, add the current character to it + } else { + arg += c; } // If the current character is a space and we are not inside a string, parse the current argument // and start a new one - } else if (c === KEY.SPACE && quotes.length === 0) { + } else if (c === KEY.SPACE && lastQuote === "") { args.push(parseArg(arg)); arg = ""; From 8e092939ce93f18ddf67f8959c9e7cea513fc894 Mon Sep 17 00:00:00 2001 From: RevanProdigalKnight Date: Fri, 13 May 2022 21:44:32 -0600 Subject: [PATCH 6/6] Fix indentation --- src/Terminal/Parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts index 9e44f250c..1ca4bb488 100644 --- a/src/Terminal/Parser.ts +++ b/src/Terminal/Parser.ts @@ -64,7 +64,7 @@ export function ParseCommand(command: string): (string | number | boolean)[] { lastQuote = c; // Otherwise if we're in a string argument, add the current character to it } else { - arg += c; + arg += c; } // If the current character is a space and we are not inside a string, parse the current argument // and start a new one