Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
Merge pull request #5861 from trufflesuite/shell-game
Browse files Browse the repository at this point in the history
Properly handle quotes, backslashes, etc in commands at console
  • Loading branch information
haltman-at authored Feb 7, 2023
2 parents a15be77 + 08b6477 commit 5a88a20
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 6 deletions.
88 changes: 88 additions & 0 deletions packages/core/lib/command-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,99 @@ const debugModule = require("debug");
const debug = debugModule("core:command:run");
const commands = require("./commands/commands");
const Web3 = require("web3");
const TruffleError = require("@truffle/error");

const defaultHost = "127.0.0.1";
const managedGanacheDefaultPort = 9545;
const managedGanacheDefaultNetworkId = 5777;
const managedDashboardDefaultPort = 24012;

//takes a string and splits it into arguments, shell-style, while
//taking account of quotes and escapes; the escape character can be
//customized (you can also pass in more than one valid escape character)
function parseQuotesAndEscapes(args, escapeCharacters = "\\") {
const quoteCharacters = "\"'"; //note we will handle the two quote types differently
let argArray = [];
let currentArg = "";
let currentQuote = undefined;
let currentEscape = undefined;
let whitespace = true; //are we currently on whitespace? start this as true to allow whitespace at beginning
for (const char of args) {
if (currentEscape !== undefined) {
//escaped character
//note that inside quotes, we don't allow escaping everything;
//outside quotes, we allow escaping anything
if (currentQuote === '"') {
//inside a double-quote case
if (char === currentQuote) {
currentArg += char; //an escaped quote
} else {
//attempted to escape something not the current quote;
//don't treat it as an escape, include the escape char as well
currentArg += currentEscape + char;
}
} else {
//outside a quote case
//(note there's no single-quote case because we can't reach here
//in that case; currentEscape can't get set inside single quotes)
currentArg += char; //just the escaped character
}
currentEscape = undefined;
whitespace = false; //(this is not strictly necessary, but for clarity)
} else if (escapeCharacters.includes(char) && currentQuote !== "'") {
//(unescaped) escape character
//(again, inside single quotes, there is no escaping, so we just treat
//as ordinary character in that case)
currentEscape = char;
whitespace = false;
} else if (currentQuote !== undefined) {
//quoted character (excluding escape/escaped chars)
if (currentQuote === char) {
//closing quote
currentQuote = undefined;
} else {
//ordinary quoted character, including quote of non-matching type
currentArg += char;
}
whitespace = false; //again not necessary, included for clarity
} else if (quoteCharacters.includes(char)) {
//(unescaped) opening quote (closing quotes & quoted quotes handled above)
currentQuote = char;
whitespace = false;
} else if (char.match(/\s/)) {
//(unescaped) whitespace
if (!whitespace) {
//if we're already on whitespace, we don't need
//to do anything, this is just more whitespace.
//if however we're transitioning to whitespace, that means we need
//to split arguments here.
argArray.push(currentArg);
currentArg = "";
whitespace = true;
}
} else {
//default case -- ordinary character
currentArg += char;
whitespace = false;
}
}
//having reached the end of the string, let's check for unterminated quotes & such
if (currentQuote !== undefined) {
throw new TruffleError(`Error: quote with ${currentQuote} not terminated`);
}
if (currentEscape !== undefined) {
throw new TruffleError(
`Error: line ended with escape character ${currentEscape}`
);
}
//now, we push our final argument,
//assuming of course that it's nonempty
if (currentArg !== "") {
argArray.push(currentArg);
}
return argArray;
}

// this function takes an object with an array of input strings, an options
// object, and a boolean determining whether we allow inexact matches for
// command names - it returns an object with the command name, the run method,
Expand Down Expand Up @@ -312,6 +399,7 @@ const deriveConfigEnvironment = function (detectedConfig, network, url) {

module.exports = {
displayGeneralHelp,
parseQuotesAndEscapes,
getCommand,
prepareOptions,
runCommand,
Expand Down
10 changes: 8 additions & 2 deletions packages/core/lib/console-child.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
const TruffleError = require("@truffle/error");
const Config = require("@truffle/config");
const yargs = require("yargs");
const { deriveConfigEnvironment } = require("./command-utils");
const path = require("path");
const {
deriveConfigEnvironment,
parseQuotesAndEscapes
} = require("./command-utils");

// we split off the part Truffle cares about and need to convert to an array
const input = process.argv[2].split(" -- ");
const inputStrings = input[1].split(" ");
const escapeCharacters = path.sep === "\\" ? "^`" : "\\"; //set escape character
//based on current OS; backslash for Unix, caret or grave for Windows
const inputStrings = parseQuotesAndEscapes(input[1], escapeCharacters); //note this shouldn't error since it's a recomputation

// we need to make sure this function exists so ensjs doesn't complain as it requires
// getRandomValues for some functionalities - webpack strips out the crypto lib
Expand Down
9 changes: 6 additions & 3 deletions packages/core/lib/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const EventEmitter = require("events");
const { spawn } = require("child_process");
const Require = require("@truffle/require");
const debug = require("debug")("console");
const { getCommand } = require("./command-utils");
const { getCommand, parseQuotesAndEscapes } = require("./command-utils");
const validTruffleCommands = require("./commands/commands");

// Create an expression that returns a string when evaluated
Expand Down Expand Up @@ -400,14 +400,17 @@ class Console extends EventEmitter {
async interpret(input, context, filename, callback) {
const processedInput = processInput(input, this.allowedCommands);
if (
this.allowedCommands.includes(processedInput.split(" ")[0]) &&
this.allowedCommands.includes(processedInput.split(/\s+/)[0]) &&
getCommand({
inputStrings: processedInput.split(" "),
inputStrings: processedInput.split(/\s+/),
options: {},
noAliases: this.options.noAliases
}) !== null
) {
try {
parseQuotesAndEscapes(processedInput); //we're just doing this to see
//if it errors. unfortunately we need to throw out the result and recompute
//it afterward (but the input string is probably short so it's OK).
await this.runSpawn(processedInput, this.options);
} catch (error) {
// Perform error handling ourselves.
Expand Down
59 changes: 58 additions & 1 deletion packages/core/test/lib/command-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ const fs = require("fs-extra");
const path = require("path");
const tmp = require("tmp");
const TruffleConfig = require("@truffle/config");
const { deriveConfigEnvironment } = require("../../lib/command-utils");
const {
deriveConfigEnvironment,
parseQuotesAndEscapes
} = require("../../lib/command-utils");

let config;

Expand Down Expand Up @@ -96,4 +99,58 @@ describe("command-utils", function () {
assert.equal(cfg.networks.develop.network_id, 5777);
});
});

describe("parseQuotesAndEscapes", function () {
it("splits on whitespace", function () {
const parsed = parseQuotesAndEscapes(" abc def ghi ");
assert.deepEqual(parsed, ["abc", "def", "ghi"]);
});
it("respects quotes and escapes", function () {
const parsed = parseQuotesAndEscapes("abc'd \"e'\"f 'g\"\\ hi");
assert.deepEqual(parsed, ["abcd \"ef 'g hi"]);
});
it("escapes correctly outside of a quote", function () {
const parsed = parseQuotesAndEscapes("ab\\c\\'\\\"\\\\ ");
assert.deepEqual(parsed, ["abc'\"\\"]);
});
it("escapes correctly inside a double-quote", function () {
const parsed = parseQuotesAndEscapes('"ab\\"c\\d"');
assert.deepEqual(parsed, ['ab"c\\d']);
});
it("does not escape inside a single quote", function () {
const parsed = parseQuotesAndEscapes("'abc\\'");
assert.deepEqual(parsed, ["abc\\"]);
});
it("allows custom escapes", function () {
const parsed = parseQuotesAndEscapes("abc\\ de` f^ g `^^`", "^`");
assert.deepEqual(parsed, ["abc\\", "de f g", "^`"]);
});
it("errors on mismatched double quote", function () {
try {
parseQuotesAndEscapes('"abc\\"');
} catch (error) {
if (error.message !== 'Error: quote with " not terminated') {
throw error;
}
}
});
it("errors on mismatched single quote", function () {
try {
parseQuotesAndEscapes("'abc");
} catch (error) {
if (error.message !== "Error: quote with ' not terminated") {
throw error;
}
}
});
it("errors on escaped end-of-line", function () {
try {
parseQuotesAndEscapes("abc\\");
} catch (error) {
if (error.message !== "Error: line ended with escape character \\") {
throw error;
}
}
});
});
});

0 comments on commit 5a88a20

Please sign in to comment.