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
- Add `^` to also escape Windows metachars:
    - 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
- Fix `options` argument being mutated
  • Loading branch information
satazor committed Nov 11, 2017
1 parent a00d9e2 commit ae1a178
Show file tree
Hide file tree
Showing 24 changed files with 449 additions and 499 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/*'
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 6.0.0 - 2017-11-11

- Remove NodeJS v0.10 and v0.12 support
- Add `^` to also escape Windows metachars:
- 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)
- Fix `options` argument being mutated


## 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
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ A cross platform solution to node's spawn and spawnSync.

`$ npm install cross-spawn`

If you are using `spawnSync` on node 0.10 or older, you will also need to install `spawn-sync`:

`$ npm install spawn-sync`


## Why

Expand All @@ -35,7 +31,7 @@ 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)
- No `options.shell` support on node `<v4.8`
- It does not allow you to run `del` or `dir`
- Has problems running commands with [spaces](https://github.com/nodejs/node/issues/7367)

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 All @@ -59,18 +55,23 @@ var results = spawn.sync('npm', ['list', '-g', '-depth', '0'], { stdio: 'inherit

## Caveats

#### `options.shell` as an alternative to `cross-spawn`
### Using `options.shell` as an alternative to `cross-spawn`

Starting from node `v4.8`, `spawn` has a `shell` option that allows you run commands from within a shell. This new option solves most of the problems that `cross-spawn` attempts to solve, but:

- It's not supported in node `<v4.8`
- It has no support for shebangs on Windows
- You must manually escape the command and arguments which is very error prone, specially when passing user input

If you are using the `shell` option to spawn a command in a cross platform way, consider using `cross-spawn` instead. You have been warned.

### `options.shell` support

While `cross-spawn` adds support for `options.shell` in node `<v4.8`, all of its enhancements are disabled.

This mimics the Node.js behavior. More specifically, the command and its arguments will not be automatically escaped nor shebang support will be offered. This is by design because if you are using `options.shell` you are probably targeting a specific platform anyway and you don't want things to get into your way.


#### Shebangs
### Shebangs support

While `cross-spawn` handles shebangs on Windows, its support is limited: e.g.: it doesn't handle arguments after the path, e.g.: `#!/bin/bash -e`.

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
84 changes: 41 additions & 43 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,53 @@
'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 escape = require('./util/escape');
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;
function detectShebang(parsed) {
parsed.file = resolveCommand(parsed.command) || resolveCommand(parsed.command, true);

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

if (shebang) {
parsed.args.unshift(parsed.file);
parsed.command = shebang;

return resolveCommand(shebang) || resolveCommand(shebang, true);
}

return parsed.file;
}

function parseNonShell(parsed) {
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 commandFile = detectShebang(parsed);

if (shebang) {
parsed.args.unshift(parsed.file);
parsed.command = shebang;
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true));
} else {
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(parsed.file);
}
// We don't need a shell if the command filename is an executable
const needsShell = !isExecutableRegExp.test(commandFile);

// If a shell is required, use cmd.exe and take care of escaping everything correctly
if (needsShell) {
// Note that `forceShell` is an hidden option used only in tests
if (parsed.options.forceShell || 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 = escape.command(parsed.command);
parsed.args = parsed.args.map(escape.argument);

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 +56,17 @@ function parseNonShell(parsed) {
}

function parseShell(parsed) {
var shellCommand;

// 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 +86,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
Loading

0 comments on commit ae1a178

Please sign in to comment.