Skip to content

Commit 21431c5

Browse files
committed
Major overhaul.
- 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
1 parent a00d9e2 commit 21431c5

21 files changed

+1455
-350
lines changed

.eslintrc

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"root": true,
33
"extends": [
4-
"@satazor/eslint-config/es5",
4+
"@satazor/eslint-config/es6",
55
"@satazor/eslint-config/addons/node"
66
]
7-
}
7+
}

.travis.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: node_js
22
node_js:
3-
- '0.10'
4-
- '0.12'
53
- '4'
64
- '6'
7-
- '7'
5+
- 'node'
6+
- 'lts/*'

CHANGELOG.md

+17
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
## 6.0.0 - 2017-11-10
2+
3+
- Remove NodeJS v0.10 and v0.12 support
4+
- Change escaping on Windows to use `^` instead of quotes:
5+
- 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)
6+
- 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)
7+
- Fix `options` argument being mutated
8+
9+
10+
## 5.1.1 - 2017-02-26
11+
12+
- Fix `options.shell` support for NodeJS [v4.8](https://github.com./nodejs/node/blob/master/doc/changelogs/CHANGELOG_V4.md#4.8.0)
13+
14+
## 5.0.1 - 2016-11-04
15+
16+
- Fix `options.shell` support for NodeJS v7
17+
118
## 5.0.0 - 2016-10-30
219

320
- Add support for `options.shell`

README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Node has issues when using spawn on Windows:
3535
- It ignores [PATHEXT](https://github.com./joyent/node/issues/2318)
3636
- It does not support [shebangs](http://pt.wikipedia.org/wiki/Shebang)
3737
- No `options.shell` support on node `<v4.8`
38-
- It does not allow you to run `del` or `dir`
38+
- Has problems running commands with [spaces](https://github.com./nodejs/node/issues/7367) when `options.shell` is specified
3939

4040
All these issues are handled correctly by `cross-spawn`.
4141
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.
@@ -59,18 +59,22 @@ var results = spawn.sync('npm', ['list', '-g', '-depth', '0'], { stdio: 'inherit
5959

6060
## Caveats
6161

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

6464
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:
6565

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

7069
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.
7170

71+
### `options.shell` support
7272

73-
#### Shebangs
73+
While `cross-spawn` adds support for `options.shell` in node `<v4.8`, all of its awesome features are disabled.
74+
75+
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.
76+
77+
### Shebangs support
7478

7579
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`.
7680

appveyor.yml

+2-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ init:
1111
# what combinations to test
1212
environment:
1313
matrix:
14-
- nodejs_version: 0.10
15-
- nodejs_version: 0.12
1614
- nodejs_version: 4
1715
- nodejs_version: 6
18-
- nodejs_version: 7
16+
- nodejs_version: 8
17+
- nodejs_version: 9
1918

2019
# get the latest stable version of Node 0.STABLE.latest
2120
install:

index.js

+8-28
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
'use strict';
22

3-
var cp = require('child_process');
4-
var parse = require('./lib/parse');
5-
var enoent = require('./lib/enoent');
6-
7-
var cpSpawnSync = cp.spawnSync;
3+
const cp = require('child_process');
4+
const parse = require('./lib/parse');
5+
const enoent = require('./lib/enoent');
86

97
function spawn(command, args, options) {
10-
var parsed;
11-
var spawned;
12-
138
// Parse the arguments
14-
parsed = parse(command, args, options);
9+
const parsed = parse(command, args, options);
1510

1611
// Spawn the child process
17-
spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
12+
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
1813

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

2621
function spawnSync(command, args, options) {
27-
var parsed;
28-
var result;
29-
30-
if (!cpSpawnSync) {
31-
try {
32-
cpSpawnSync = require('spawn-sync'); // eslint-disable-line global-require
33-
} catch (ex) {
34-
throw new Error(
35-
'In order to use spawnSync on node 0.10 or older, you must ' +
36-
'install spawn-sync:\n\n' +
37-
' npm install spawn-sync --save'
38-
);
39-
}
40-
}
41-
4222
// Parse the arguments
43-
parsed = parse(command, args, options);
23+
const parsed = parse(command, args, options);
4424

4525
// Spawn the child process
46-
result = cpSpawnSync(parsed.command, parsed.args, parsed.options);
26+
const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
4727

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

5131
return result;

lib/enoent.js

+10-28
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,35 @@
11
'use strict';
22

3-
var isWin = process.platform === 'win32';
4-
var resolveCommand = require('./util/resolveCommand');
5-
6-
var isNode10 = process.version.indexOf('v0.10.') === 0;
3+
const isWin = process.platform === 'win32';
74

85
function notFoundError(command, syscall) {
9-
var err;
10-
11-
err = new Error(syscall + ' ' + command + ' ENOENT');
12-
err.code = err.errno = 'ENOENT';
13-
err.syscall = syscall + ' ' + command;
14-
15-
return err;
6+
return Object.assign(new Error(`${syscall} ${command} ENOENT`), {
7+
code: 'ENOENT',
8+
errno: 'ENOENT',
9+
syscall: `${syscall} ${command}`,
10+
});
1611
}
1712

1813
function hookChildProcess(cp, parsed) {
19-
var originalEmit;
20-
2114
if (!isWin) {
2215
return;
2316
}
2417

25-
originalEmit = cp.emit;
26-
cp.emit = function (name, arg1) {
27-
var err;
18+
const originalEmit = cp.emit;
2819

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

3527
if (err) {
3628
return originalEmit.call(cp, 'error', err);
3729
}
3830
}
3931

40-
return originalEmit.apply(cp, arguments);
32+
return originalEmit.apply(cp, arguments); // eslint-disable-line prefer-rest-params
4133
};
4234
}
4335

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

57-
// If we are in node 10, then we are using spawn-sync; if it exited
58-
// with -1 it probably means that the command does not exist
59-
if (isNode10 && status === -1) {
60-
parsed.file = isWin ? parsed.file : resolveCommand(parsed.original);
61-
62-
if (!parsed.file) {
63-
return notFoundError(parsed.original, 'spawnSync');
64-
}
65-
}
66-
6749
return null;
6850
}
6951

lib/parse.js

+38-43
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,50 @@
11
'use strict';
22

3-
var resolveCommand = require('./util/resolveCommand');
4-
var hasEmptyArgumentBug = require('./util/hasEmptyArgumentBug');
5-
var escapeArgument = require('./util/escapeArgument');
6-
var escapeCommand = require('./util/escapeCommand');
7-
var readShebang = require('./util/readShebang');
3+
const resolveCommand = require('./util/resolveCommand');
4+
const escapeArgument = require('./util/escapeArgument');
5+
const readShebang = require('./util/readShebang');
86

9-
var isWin = process.platform === 'win32';
10-
var skipShellRegExp = /\.(?:com|exe)$/i;
7+
const isWin = process.platform === 'win32';
8+
const isExecutableRegExp = /\.(?:com|exe)$/i;
119

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

16-
function parseNonShell(parsed) {
17-
var shebang;
18-
var needsShell;
19-
var applyQuotes;
15+
function detectShebang(parsed) {
16+
const file = resolveCommand(parsed.command) || resolveCommand(parsed.command, true);
17+
const shebang = file && readShebang(parsed.file);
18+
19+
if (shebang) {
20+
parsed.args.unshift(parsed.file);
21+
parsed.command = shebang;
2022

23+
return resolveCommand(shebang) || resolveCommand(shebang, true);
24+
}
25+
26+
return file;
27+
}
28+
29+
function parseNonShell(parsed) {
2130
if (!isWin) {
2231
return parsed;
2332
}
2433

2534
// Detect & add support for shebangs
26-
parsed.file = resolveCommand(parsed.command);
27-
parsed.file = parsed.file || resolveCommand(parsed.command, true);
28-
shebang = parsed.file && readShebang(parsed.file);
29-
30-
if (shebang) {
31-
parsed.args.unshift(parsed.file);
32-
parsed.command = shebang;
33-
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true));
34-
} else {
35-
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(parsed.file);
36-
}
35+
const resolvedCommand = detectShebang(parsed);
3736

3837
// If a shell is required, use cmd.exe and take care of escaping everything correctly
38+
const needsShell = !isExecutableRegExp.test(resolvedCommand);
39+
3940
if (needsShell) {
4041
// Escape command & arguments
41-
applyQuotes = (parsed.command !== 'echo'); // Do not quote arguments for the special "echo" command
42-
parsed.command = escapeCommand(parsed.command);
43-
parsed.args = parsed.args.map(function (arg) {
44-
return escapeArgument(arg, applyQuotes);
45-
});
46-
47-
// Make use of cmd.exe
48-
parsed.args = ['/d', '/s', '/c', '"' + parsed.command + (parsed.args.length ? ' ' + parsed.args.join(' ') : '') + '"'];
42+
parsed.command = escapeArgument(parsed.command);
43+
parsed.args = parsed.args.map(escapeArgument);
44+
45+
const shellCommand = [parsed.command].concat(parsed.args).join(' ');
46+
47+
parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`];
4948
parsed.command = process.env.comspec || 'cmd.exe';
5049
parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped
5150
}
@@ -54,19 +53,17 @@ function parseNonShell(parsed) {
5453
}
5554

5655
function parseShell(parsed) {
57-
var shellCommand;
58-
5956
// If node supports the shell option, there's no need to mimic its behavior
6057
if (supportsShellOption) {
6158
return parsed;
6259
}
6360

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

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

8885
function parse(command, args, options) {
89-
var parsed;
90-
9186
// Normalize arguments, similar to nodejs
9287
if (args && !Array.isArray(args)) {
9388
options = args;
9489
args = null;
9590
}
9691

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

10095
// Build our parsed object
101-
parsed = {
102-
command: command,
103-
args: args,
104-
options: options,
96+
const parsed = {
97+
command,
98+
args,
99+
options,
105100
file: undefined,
106101
original: command,
107102
};

0 commit comments

Comments
 (0)