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
- Fix `options` argument being mutated
  • Loading branch information
satazor committed Nov 11, 2017
1 parent a00d9e2 commit 73d4bed
Show file tree
Hide file tree
Showing 21 changed files with 1,449 additions and 353 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-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)
- 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
16 changes: 8 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) when `options.shell` is specified

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,22 @@ 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 awesome features are disabled.

This means that no shebangs are supported nor the command and arguments are escaped. This is by design because if you are using `options.shell` you are probably targeting a specific platform anyway.

#### 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
81 changes: 39 additions & 42 deletions lib/parse.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,52 @@
'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;
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) {
// 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 +55,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 +85,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 73d4bed

Please sign in to comment.