Skip to content

Commit

Permalink
Major overhaul.
Browse files Browse the repository at this point in the history
- Remove NodeJS v0.10 and v0.12 support
- Change escaping on Windows to use `^` instead of quotes:
    - Fix a bug that made it impossible to escape an argument that contained quotes followed by `>` or other special chars, e.g.: `"foo|bar"`, fixes #82
    - Fix a bug were a command containing `%x%` would be replaced with the contents of the `x` environment variable, fixes #51
- Add a work around for a NodeJS bug when spawning a command with spaces when `options.shell` was enabled, fixes #77
- Fix `options` argument being mutated
- Remove support for running `echo` on Windows
  • Loading branch information
satazor committed Nov 11, 2017
1 parent a00d9e2 commit fe1b927
Show file tree
Hide file tree
Showing 21 changed files with 1,442 additions and 338 deletions.
4 changes: 2 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"root": true,
"extends": [
"@satazor/eslint-config/es5",
"@satazor/eslint-config/es6",
"@satazor/eslint-config/addons/node"
]
}
}
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: node_js
node_js:
- '0.10'
- '0.12'
- '4'
- '6'
- '7'
- 'node'
- 'lts/*'
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## 6.0.0 - 2017-11-10

- Remove NodeJS v0.10 and v0.12 support
- Change escaping on Windows to use `^` instead of quotes:
- Fix a bug that made it impossible to escape an argument that contained quotes followed by `>` or other special chars, e.g.: `"foo|bar"`, fixes [#82](https://github.com/IndigoUnited/node-cross-spawn/issues/82)
- Fix a bug were a command containing `%x%` would be replaced with the contents of the `x` environment variable, fixes [#51](https://github.com/IndigoUnited/node-cross-spawn/issues/51)
- Add a work around for a NodeJS bug when spawning a command with spaces when `options.shell` was enabled, fixes [#77](https://github.com/IndigoUnited/node-cross-spawn/issues/77)
- Fix `options` argument being mutated
- Remove support for running `echo` on Windows


## 5.1.1 - 2017-02-26

- Fix `options.shell` support for NodeJS [v4.8](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V4.md#4.8.0)

## 5.0.1 - 2016-11-04

- Fix `options.shell` support for NodeJS v7

## 5.0.0 - 2016-10-30

- Add support for `options.shell`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ Node has issues when using spawn on Windows:

- It ignores [PATHEXT](https://github.com/joyent/node/issues/2318)
- It does not support [shebangs](http://pt.wikipedia.org/wiki/Shebang)
- Has problems running commands with [spaces](https://github.com/nodejs/node/issues/7367)
- No `options.shell` support on node `<v4.8`
- It does not allow you to run `del` or `dir`

All these issues are handled correctly by `cross-spawn`.
There are some known modules, such as [win-spawn](https://github.com/ForbesLindesay/win-spawn), that try to solve this but they are either broken or provide faulty escaping of shell arguments.
Expand Down
5 changes: 2 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ init:
# what combinations to test
environment:
matrix:
- nodejs_version: 0.10
- nodejs_version: 0.12
- nodejs_version: 4
- nodejs_version: 6
- nodejs_version: 7
- nodejs_version: 8
- nodejs_version: 9

# get the latest stable version of Node 0.STABLE.latest
install:
Expand Down
36 changes: 8 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
'use strict';

var cp = require('child_process');
var parse = require('./lib/parse');
var enoent = require('./lib/enoent');

var cpSpawnSync = cp.spawnSync;
const cp = require('child_process');
const parse = require('./lib/parse');
const enoent = require('./lib/enoent');

function spawn(command, args, options) {
var parsed;
var spawned;

// Parse the arguments
parsed = parse(command, args, options);
const parsed = parse(command, args, options);

// Spawn the child process
spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);

// Hook into child process "exit" event to emit an error if the command
// does not exists, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16
Expand All @@ -24,28 +19,13 @@ function spawn(command, args, options) {
}

function spawnSync(command, args, options) {
var parsed;
var result;

if (!cpSpawnSync) {
try {
cpSpawnSync = require('spawn-sync'); // eslint-disable-line global-require
} catch (ex) {
throw new Error(
'In order to use spawnSync on node 0.10 or older, you must ' +
'install spawn-sync:\n\n' +
' npm install spawn-sync --save'
);
}
}

// Parse the arguments
parsed = parse(command, args, options);
const parsed = parse(command, args, options);

// Spawn the child process
result = cpSpawnSync(parsed.command, parsed.args, parsed.options);
const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);

// Analyze if the command does not exists, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16
// Analyze if the command does not exist, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);

return result;
Expand Down
38 changes: 10 additions & 28 deletions lib/enoent.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,35 @@
'use strict';

var isWin = process.platform === 'win32';
var resolveCommand = require('./util/resolveCommand');

var isNode10 = process.version.indexOf('v0.10.') === 0;
const isWin = process.platform === 'win32';

function notFoundError(command, syscall) {
var err;

err = new Error(syscall + ' ' + command + ' ENOENT');
err.code = err.errno = 'ENOENT';
err.syscall = syscall + ' ' + command;

return err;
return Object.assign(new Error(`${syscall} ${command} ENOENT`), {
code: 'ENOENT',
errno: 'ENOENT',
syscall: `${syscall} ${command}`,
});
}

function hookChildProcess(cp, parsed) {
var originalEmit;

if (!isWin) {
return;
}

originalEmit = cp.emit;
cp.emit = function (name, arg1) {
var err;
const originalEmit = cp.emit;

cp.emit = function (name, arg1) {
// If emitting "exit" event and exit code is 1, we need to check if
// the command exists and emit an "error" instead
// See: https://github.com/IndigoUnited/node-cross-spawn/issues/16
if (name === 'exit') {
err = verifyENOENT(arg1, parsed, 'spawn');
const err = verifyENOENT(arg1, parsed, 'spawn');

if (err) {
return originalEmit.call(cp, 'error', err);
}
}

return originalEmit.apply(cp, arguments);
return originalEmit.apply(cp, arguments); // eslint-disable-line prefer-rest-params
};
}

Expand All @@ -54,16 +46,6 @@ function verifyENOENTSync(status, parsed) {
return notFoundError(parsed.original, 'spawnSync');
}

// If we are in node 10, then we are using spawn-sync; if it exited
// with -1 it probably means that the command does not exist
if (isNode10 && status === -1) {
parsed.file = isWin ? parsed.file : resolveCommand(parsed.original);

if (!parsed.file) {
return notFoundError(parsed.original, 'spawnSync');
}
}

return null;
}

Expand Down
64 changes: 30 additions & 34 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,46 @@
'use strict';

var resolveCommand = require('./util/resolveCommand');
var hasEmptyArgumentBug = require('./util/hasEmptyArgumentBug');
var escapeArgument = require('./util/escapeArgument');
var escapeCommand = require('./util/escapeCommand');
var readShebang = require('./util/readShebang');
const resolveCommand = require('./util/resolveCommand');
const escapeArgument = require('./util/escapeArgument');
const readShebang = require('./util/readShebang');

var isWin = process.platform === 'win32';
var skipShellRegExp = /\.(?:com|exe)$/i;
const isWin = process.platform === 'win32';
const isExecutableRegExp = /\.(?:com|exe)$/i;

// Supported in Node >= 6 and >= 4.8
var supportsShellOption = parseInt(process.version.substr(1).split('.')[0], 10) >= 6 ||
parseInt(process.version.substr(1).split('.')[0], 10) === 4 && parseInt(process.version.substr(1).split('.')[1], 10) >= 8;
const supportsShellOption =
parseInt(process.version.substr(1).split('.')[0], 10) >= 6 ||
(parseInt(process.version.substr(1).split('.')[0], 10) === 4 && parseInt(process.version.substr(1).split('.')[1], 10) >= 8);

function parseNonShell(parsed) {
var shebang;
var needsShell;
var applyQuotes;

if (!isWin) {
return parsed;
}

// Detect & add support for shebangs
parsed.file = resolveCommand(parsed.command);
parsed.file = parsed.file || resolveCommand(parsed.command, true);
shebang = parsed.file && readShebang(parsed.file);

const shebang = parsed.file && readShebang(parsed.file);
let needsShell;

if (shebang) {
parsed.args.unshift(parsed.file);
parsed.command = shebang;
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true));
needsShell = !isExecutableRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true));
} else {
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(parsed.file);
needsShell = !isExecutableRegExp.test(parsed.file);
}

// If a shell is required, use cmd.exe and take care of escaping everything correctly
if (needsShell) {
// Escape command & arguments
applyQuotes = (parsed.command !== 'echo'); // Do not quote arguments for the special "echo" command
parsed.command = escapeCommand(parsed.command);
parsed.args = parsed.args.map(function (arg) {
return escapeArgument(arg, applyQuotes);
});

// Make use of cmd.exe
parsed.args = ['/d', '/s', '/c', '"' + parsed.command + (parsed.args.length ? ' ' + parsed.args.join(' ') : '') + '"'];
parsed.command = escapeArgument(parsed.command);
parsed.args = parsed.args.map(escapeArgument);

const shellCommand = [parsed.command].concat(parsed.args).join(' ');

parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`];
parsed.command = process.env.comspec || 'cmd.exe';
parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped
}
Expand All @@ -54,19 +49,22 @@ function parseNonShell(parsed) {
}

function parseShell(parsed) {
var shellCommand;
// Work around a bug on NodeJS when command has spaces by escaping the command
// in both Unix and Windows
// See: https://github.com/IndigoUnited/node-cross-spawn/issues/77
parsed.command = escapeArgument(parsed.command);

// If node supports the shell option, there's no need to mimic its behavior
if (supportsShellOption) {
return parsed;
}

// Mimic node shell option, see: https://github.com/nodejs/node/blob/b9f6a2dc059a1062776133f3d4fd848c4da7d150/lib/child_process.js#L335
shellCommand = [parsed.command].concat(parsed.args).join(' ');
const shellCommand = [parsed.command].concat(parsed.args).join(' ');

if (isWin) {
parsed.command = typeof parsed.options.shell === 'string' ? parsed.options.shell : process.env.comspec || 'cmd.exe';
parsed.args = ['/d', '/s', '/c', '"' + shellCommand + '"'];
parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`];
parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped
} else {
if (typeof parsed.options.shell === 'string') {
Expand All @@ -86,22 +84,20 @@ function parseShell(parsed) {
// ------------------------------------------------

function parse(command, args, options) {
var parsed;

// Normalize arguments, similar to nodejs
if (args && !Array.isArray(args)) {
options = args;
args = null;
}

args = args ? args.slice(0) : []; // Clone array to avoid changing the original
options = options || {};
options = Object.assign({}, options); // Clone object to avoid changing the original

// Build our parsed object
parsed = {
command: command,
args: args,
options: options,
const parsed = {
command,
args,
options,
file: undefined,
original: command,
};
Expand Down
42 changes: 19 additions & 23 deletions lib/util/escapeArgument.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
'use strict';

function escapeArgument(arg, quote) {
const isWin = process.platform === 'win32';

function escapeArgumentWindows(arg) {
// Convert to string
arg = '' + arg;

// If we are not going to quote the argument,
// escape shell metacharacters, including double and single quotes:
if (!quote) {
arg = arg.replace(/([()%!^<>&|;,"'\s])/g, '^$1');
} else {
// Sequence of backslashes followed by a double quote:
// double up all the backslashes and escape the double quote
arg = arg.replace(/(\\*)"/g, '$1$1\\"');

// 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');

// All other backslashes occur literally

// Quote the whole thing:
arg = '"' + arg + '"';
}
arg = `${arg}`;

// Escape quotes with \^
arg = arg.replace(/"/g, '\\^$1');

// Escape other meta chars with ^
arg = arg.replace(/([()%!^<>&|;,\s])/g, '^$1');

return arg;
}

module.exports = escapeArgument;
function escapeArgumentUnix(arg) {
if (/^[a-z0-9_-]+$/i.test(arg)) {
return arg;
}

return `"${arg.replace('\'', "'\\'")}"`;
}

module.exports = isWin ? escapeArgumentWindows : escapeArgumentUnix;
12 changes: 0 additions & 12 deletions lib/util/escapeCommand.js

This file was deleted.

Loading

0 comments on commit fe1b927

Please sign in to comment.