diff --git a/src/shellEscape.ts b/src/shellEscape.ts new file mode 100644 index 00000000..249f2535 --- /dev/null +++ b/src/shellEscape.ts @@ -0,0 +1,30 @@ +import {OSType, getOSType} from './utils'; + +export function shellEscape(args: Array): string { + var output: Array = []; + + if (getOSType() === OSType.windows) { + args.forEach(function(arg) { + // Check if the argument is a file path + const isFilePath = /^([a-zA-Z]:)?(\\[^<>:"/\\|?*]+)+\.exe$/.test(arg); + + if (!isFilePath && /[^A-Za-z0-9_\/:=-]/.test(arg)) { + arg = '"' + arg.replace(/"/g, '\\"') + '"'; + arg = arg.replace(/^(?:"")+/g, '') // unduplicate double-quote at the beginning + .replace(/\\"""/g, '\\"'); // remove non-escaped double-quote if there are enclosed between 2 escaped + } + output.push(arg); + }); + return output.join(' '); + } else { + args.forEach(function(arg) { + if (/[^A-Za-z0-9_\/:=-]/.test(arg)) { + arg = "'" + arg.replace(/'/g,"'\\''") + "'"; + arg = arg.replace(/^(?:'')+/g, '') // unduplicate single-quote at the beginning + .replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped + }; + output.push(arg); + }); + return output.join(' '); + }; +}; diff --git a/src/stripeTerminal.ts b/src/stripeTerminal.ts index 1a7d3014..d5e4a933 100644 --- a/src/stripeTerminal.ts +++ b/src/stripeTerminal.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import {StripeClient} from './stripeClient'; +import {shellEscape} from './shellEscape'; type SupportedStripeCommand = 'events' | 'listen' | 'logs' | 'login' | 'trigger'; @@ -29,9 +30,9 @@ export class StripeTerminal { } } - const globalCLIFLags = this.getGlobalCLIFlags(); + const globalCLIFlags = this.getGlobalCLIFlags(); - const commandString = [cliPath, command, ...args, ...globalCLIFLags].join(' '); + const commandString = shellEscape([cliPath, command, ...args, ...globalCLIFlags]); try { const terminal = await this.createTerminal(); diff --git a/test/suite/shellEscape.test.ts b/test/suite/shellEscape.test.ts new file mode 100644 index 00000000..15297c6f --- /dev/null +++ b/test/suite/shellEscape.test.ts @@ -0,0 +1,61 @@ +/* eslint-disable quotes */ +import * as assert from 'assert'; +import * as shellEscape from '../../src/shellEscape'; +import * as sinon from 'sinon'; +import * as utils from '../../src/utils'; + + +suite('shellEscape', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('shellEscape', () => { + test('non windows case: flag with spaces', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.macOSarm); + const output = shellEscape.shellEscape(['--project-name', 'test | whoami']); // test | whoami + assert.strictEqual(output, `--project-name 'test | whoami'`); // --project-name 'test | whoami' + }); + test('non windows case: flag with single quote around entire arg', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.macOSarm); + const output = shellEscape.shellEscape(['--project-name', `'test name'`]); // 'test name' + assert.strictEqual(output, `--project-name \\''test name'\\'`); // --project-name \''test name'\' + }); + test('non windows case: flag with double quote', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.macOSarm); + const output = shellEscape.shellEscape(['--project-name', `test "name"`]); // test "name" + assert.strictEqual(output, `--project-name 'test "name"'`); // --project-name 'test "name"' + }); + test('non windows case: flag with lots of quotes', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.macOSarm); + const output = shellEscape.shellEscape(['--project-name', `'test's "name"'`]); // 'test's "name"' + assert.strictEqual(output, `--project-name \\''test'\\''s "name"'\\'`); // --project-name \''test'\''s "name"'\' + }); + test.only('windows case: flag with space', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.windows); + const output = shellEscape.shellEscape(['--project-name', 'test | whoami']); // test | whoami + assert.strictEqual(output, `--project-name "test | whoami"`); // --project-name "test | whoami" + }); + test.only('windows case: flag with single quote around entire arg', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.windows); + const output = shellEscape.shellEscape(['--project-name', `'test name'`]); // 'test name' + assert.strictEqual(output, `--project-name "'test name'"`); // --project-name "'test name'" + }); + test.only('windows case: flag with double quote', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.windows); + const output = shellEscape.shellEscape(['--project-name', `test "name"`]); // test "name" + assert.strictEqual(output, `--project-name "test \\"name\\""`); // --project-name "test \"name\"" + }); + test.only('windows case: flag with lots of quotes', () => { + sandbox.stub(utils, 'getOSType').returns(utils.OSType.windows); + const output = shellEscape.shellEscape(['--project-name', `'test's "name"'`]); // 'test's "name"' + assert.strictEqual(output, `--project-name "'test's \\"name\\"'"`); // --project-name "'test's \"name\"'" + }); + }); +});