Skip to content

Commit 3226dfb

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 3226dfb

24 files changed

+448
-499
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-11
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

+9-8
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ A cross platform solution to node's spawn and spawnSync.
2323

2424
`$ npm install cross-spawn`
2525

26-
If you are using `spawnSync` on node 0.10 or older, you will also need to install `spawn-sync`:
27-
28-
`$ npm install spawn-sync`
29-
3026

3127
## Why
3228

@@ -35,7 +31,7 @@ Node has issues when using spawn on Windows:
3531
- It ignores [PATHEXT](https://github.com./joyent/node/issues/2318)
3632
- It does not support [shebangs](http://pt.wikipedia.org/wiki/Shebang)
3733
- No `options.shell` support on node `<v4.8`
38-
- It does not allow you to run `del` or `dir`
34+
- Has problems running commands with [spaces](https://github.com./nodejs/node/issues/7367)
3935

4036
All these issues are handled correctly by `cross-spawn`.
4137
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 +55,23 @@ var results = spawn.sync('npm', ['list', '-g', '-depth', '0'], { stdio: 'inherit
5955

6056
## Caveats
6157

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

6460
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:
6561

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

7065
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.
7166

67+
### `options.shell` support
68+
69+
While `cross-spawn` adds support for `options.shell` in node `<v4.8`, all of its enhancements are disabled.
70+
71+
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.
72+
7273

73-
#### Shebangs
74+
### Shebangs support
7475

7576
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`.
7677

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

+41-43
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,53 @@
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 escape = require('./util/escape');
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+
parsed.file = resolveCommand(parsed.command) || resolveCommand(parsed.command, true);
17+
18+
const shebang = parsed.file && readShebang(parsed.file);
19+
20+
if (shebang) {
21+
parsed.args.unshift(parsed.file);
22+
parsed.command = shebang;
23+
24+
return resolveCommand(shebang) || resolveCommand(shebang, true);
25+
}
26+
27+
return parsed.file;
28+
}
2029

30+
function parseNonShell(parsed) {
2131
if (!isWin) {
2232
return parsed;
2333
}
2434

2535
// 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);
36+
const commandFile = detectShebang(parsed);
2937

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-
}
38+
// We don't need a shell if the command filename is an executable
39+
const needsShell = !isExecutableRegExp.test(commandFile);
3740

3841
// If a shell is required, use cmd.exe and take care of escaping everything correctly
39-
if (needsShell) {
42+
// Note that `forceShell` is an hidden option used only in tests
43+
if (parsed.options.forceShell || needsShell) {
4044
// 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(' ') : '') + '"'];
45+
parsed.command = escape.command(parsed.command);
46+
parsed.args = parsed.args.map(escape.argument);
47+
48+
const shellCommand = [parsed.command].concat(parsed.args).join(' ');
49+
50+
parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`];
4951
parsed.command = process.env.comspec || 'cmd.exe';
5052
parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped
5153
}
@@ -54,19 +56,17 @@ function parseNonShell(parsed) {
5456
}
5557

5658
function parseShell(parsed) {
57-
var shellCommand;
58-
5959
// If node supports the shell option, there's no need to mimic its behavior
6060
if (supportsShellOption) {
6161
return parsed;
6262
}
6363

6464
// 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(' ');
65+
const shellCommand = [parsed.command].concat(parsed.args).join(' ');
6666

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

8888
function parse(command, args, options) {
89-
var parsed;
90-
9189
// Normalize arguments, similar to nodejs
9290
if (args && !Array.isArray(args)) {
9391
options = args;
9492
args = null;
9593
}
9694

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

10098
// Build our parsed object
101-
parsed = {
102-
command: command,
103-
args: args,
104-
options: options,
99+
const parsed = {
100+
command,
101+
args,
102+
options,
105103
file: undefined,
106104
original: command,
107105
};

0 commit comments

Comments
 (0)