Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions lib/util/escape.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ function escapeArgument(arg, doubleEscapeMetaChars) {
// Please see https://github.com/moxystudio/node-cross-spawn/pull/160 for more information

// Sequence of backslashes followed by a double quote:
// double up all the backslashes and escape the double quote
arg = arg.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');

// double up all the backslashes and escape the double quote.
// Sequence of backslashes followed by the end of the string
// (which will become a double quote later):
// double up all the backslashes
arg = arg.replace(/(?=(\\+?)?)\1$/, '$1$1');

// All other backslashes occur literally
// double up all the backslashes.
// All other backslashes occur literally.
// Whole runs of backslashes are matched first so that the engine never
// rescans a run, which keeps this linear on any input.
arg = arg.replace(/(\\*)(?:(")|($))|\\+/g, (match, backslashes, quote, end) => {
if (quote === undefined && end === undefined) {
return match;
}

return backslashes + backslashes + (quote ? '\\"' : '');
});

// Quote the whole thing:
arg = `"${arg}"`;
Expand Down
30 changes: 30 additions & 0 deletions lib/util/escape.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

const escape = require('./escape');

describe('escape.argument', () => {
it('should double the whole run of backslashes before a double quote', () => {
expect(escape.argument('a\\"b')).toBe('^"a\\\\\\^"b^"');
expect(escape.argument('a\\\\"b')).toBe('^"a\\\\\\\\\\^"b^"');
expect(escape.argument('a\\\\\\"b')).toBe('^"a\\\\\\\\\\\\\\^"b^"');
});

it('should double the whole run of backslashes at the end of the argument', () => {
expect(escape.argument('a\\')).toBe('^"a\\\\^"');
expect(escape.argument('a\\\\')).toBe('^"a\\\\\\\\^"');
expect(escape.argument('a\\\\\\')).toBe('^"a\\\\\\\\\\\\^"');
});

it('should leave other backslashes untouched', () => {
expect(escape.argument('C:\\a\\\\b')).toBe('^"C:\\a\\\\b^"');
});

it('should run in linear time on long runs of backslashes', () => {
const start = Date.now();

escape.argument(`${'\\'.repeat(200000)}a`);
escape.argument(`"${'\\'.repeat(200000)}a`);

expect(Date.now() - start).toBeLessThan(1000);
});
});
12 changes: 12 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,18 @@ run.methods.forEach((method) => {

expect(stdout).toBe(arg);
});

it('should keep runs of backslashes when executing `node_modules/.bin/<file>.cmd`', async () => {
mkdirp.sync(`${__dirname}/fixtures/node_modules/.bin`);
fs.writeFileSync(`${__dirname}/fixtures/node_modules/.bin/echo-cmd-shim.cmd`,
fs.readFileSync(`${__dirname}/fixtures/echo-cmd-shim.cmd`));

const args = ['a\\\\', 'x\\\\" --foo \\\\', 'C:\\a\\\\', 'bar'];

const { stdout } = await run(method, `${__dirname}/fixtures/node_modules/.bin/echo-cmd-shim`, args);

expect(stdout).toBe(args.join('\n'));
});
}

it('should handle commands with names of environment variables', async () => {
Expand Down