Skip to content

Commit fe1b927

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 - 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
1 parent a00d9e2 commit fe1b927

21 files changed

+1442
-338
lines changed

.eslintrc

Lines changed: 2 additions & 2 deletions
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

Lines changed: 2 additions & 3 deletions
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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
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+
- 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)
8+
- Fix `options` argument being mutated
9+
- Remove support for running `echo` on Windows
10+
11+
12+
## 5.1.1 - 2017-02-26
13+
14+
- Fix `options.shell` support for NodeJS [v4.8](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V4.md#4.8.0)
15+
16+
## 5.0.1 - 2016-11-04
17+
18+
- Fix `options.shell` support for NodeJS v7
19+
120
## 5.0.0 - 2016-10-30
221

322
- Add support for `options.shell`

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Node has issues when using spawn on Windows:
3434

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

appveyor.yml

Lines changed: 2 additions & 3 deletions
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

Lines changed: 8 additions & 28 deletions
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

Lines changed: 10 additions & 28 deletions
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

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,46 @@
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

1615
function parseNonShell(parsed) {
17-
var shebang;
18-
var needsShell;
19-
var applyQuotes;
20-
2116
if (!isWin) {
2217
return parsed;
2318
}
2419

2520
// Detect & add support for shebangs
2621
parsed.file = resolveCommand(parsed.command);
2722
parsed.file = parsed.file || resolveCommand(parsed.command, true);
28-
shebang = parsed.file && readShebang(parsed.file);
23+
24+
const shebang = parsed.file && readShebang(parsed.file);
25+
let needsShell;
2926

3027
if (shebang) {
3128
parsed.args.unshift(parsed.file);
3229
parsed.command = shebang;
33-
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true));
30+
needsShell = !isExecutableRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true));
3431
} else {
35-
needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(parsed.file);
32+
needsShell = !isExecutableRegExp.test(parsed.file);
3633
}
3734

3835
// If a shell is required, use cmd.exe and take care of escaping everything correctly
3936
if (needsShell) {
4037
// 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(' ') : '') + '"'];
38+
parsed.command = escapeArgument(parsed.command);
39+
parsed.args = parsed.args.map(escapeArgument);
40+
41+
const shellCommand = [parsed.command].concat(parsed.args).join(' ');
42+
43+
parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`];
4944
parsed.command = process.env.comspec || 'cmd.exe';
5045
parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped
5146
}
@@ -54,19 +49,22 @@ function parseNonShell(parsed) {
5449
}
5550

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

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

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

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

8886
function parse(command, args, options) {
89-
var parsed;
90-
9187
// Normalize arguments, similar to nodejs
9288
if (args && !Array.isArray(args)) {
9389
options = args;
9490
args = null;
9591
}
9692

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

10096
// Build our parsed object
101-
parsed = {
102-
command: command,
103-
args: args,
104-
options: options,
97+
const parsed = {
98+
command,
99+
args,
100+
options,
105101
file: undefined,
106102
original: command,
107103
};

lib/util/escapeArgument.js

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
'use strict';
22

3-
function escapeArgument(arg, quote) {
3+
const isWin = process.platform === 'win32';
4+
5+
function escapeArgumentWindows(arg) {
46
// Convert to string
5-
arg = '' + arg;
6-
7-
// If we are not going to quote the argument,
8-
// escape shell metacharacters, including double and single quotes:
9-
if (!quote) {
10-
arg = arg.replace(/([()%!^<>&|;,"'\s])/g, '^$1');
11-
} else {
12-
// Sequence of backslashes followed by a double quote:
13-
// double up all the backslashes and escape the double quote
14-
arg = arg.replace(/(\\*)"/g, '$1$1\\"');
15-
16-
// Sequence of backslashes followed by the end of the string
17-
// (which will become a double quote later):
18-
// double up all the backslashes
19-
arg = arg.replace(/(\\*)$/, '$1$1');
20-
21-
// All other backslashes occur literally
22-
23-
// Quote the whole thing:
24-
arg = '"' + arg + '"';
25-
}
7+
arg = `${arg}`;
8+
9+
// Escape quotes with \^
10+
arg = arg.replace(/"/g, '\\^$1');
11+
12+
// Escape other meta chars with ^
13+
arg = arg.replace(/([()%!^<>&|;,\s])/g, '^$1');
2614

2715
return arg;
2816
}
2917

30-
module.exports = escapeArgument;
18+
function escapeArgumentUnix(arg) {
19+
if (/^[a-z0-9_-]+$/i.test(arg)) {
20+
return arg;
21+
}
22+
23+
return `"${arg.replace('\'', "'\\'")}"`;
24+
}
25+
26+
module.exports = isWin ? escapeArgumentWindows : escapeArgumentUnix;

lib/util/escapeCommand.js

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)