Skip to content

Commit c3bb4b1

Browse files
committed
child_process: add shell option to spawn()
This commit adds a shell option, to spawn() and spawnSync(). This option allows child processes to be spawned with or without a shell. The option also allows a custom shell to be defined, for compatibility with exec()'s shell option. Fixes: #1009 PR-URL: #4598 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 34daaa7 commit c3bb4b1

4 files changed

+144
-29
lines changed

doc/api/child_process.markdown

+14-5
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,12 @@ The importance of the distinction between `child_process.exec()` and
7777
`child_process.execFile()` can vary based on platform. On Unix-type operating
7878
systems (Unix, Linux, OSX) `child_process.execFile()` can be more efficient
7979
because it does not spawn a shell. On Windows, however, `.bat` and `.cmd`
80-
files are not executable on their own without a terminal and therefore cannot
81-
be launched using `child_process.execFile()` (or even `child_process.spawn()`).
82-
When running on Windows, `.bat` and `.cmd` files can only be invoked using
83-
either `child_process.exec()` or by spawning `cmd.exe` and passing the `.bat`
84-
or `.cmd` file as an argument (which is what `child_process.exec()` does).
80+
files are not executable on their own without a terminal, and therefore cannot
81+
be launched using `child_process.execFile()`. When running on Windows, `.bat`
82+
and `.cmd` files can be invoked using `child_process.spawn()` with the `shell`
83+
option set, with `child_process.exec()`, or by spawning `cmd.exe` and passing
84+
the `.bat` or `.cmd` file as an argument (which is what the `shell` option and
85+
`child_process.exec()` do).
8586

8687
```js
8788
// On Windows Only ...
@@ -277,6 +278,10 @@ not clone the current process.*
277278
[`options.detached`][])
278279
* `uid` {Number} Sets the user identity of the process. (See setuid(2).)
279280
* `gid` {Number} Sets the group identity of the process. (See setgid(2).)
281+
* `shell` {Boolean|String} If `true`, runs `command` inside of a shell. Uses
282+
'/bin/sh' on UNIX, and 'cmd.exe' on Windows. A different shell can be
283+
specified as a string. The shell should understand the `-c` switch on UNIX,
284+
or `/s /c` on Windows. Defaults to `false` (no shell).
280285
* return: {ChildProcess object}
281286

282287
The `child_process.spawn()` method spawns a new process using the given
@@ -581,6 +586,10 @@ throw. The [`Error`][] object will contain the entire result from
581586
* `maxBuffer` {Number} largest amount of data (in bytes) allowed on stdout or
582587
stderr - if exceeded child process is killed
583588
* `encoding` {String} The encoding used for all stdio inputs and outputs. (Default: 'buffer')
589+
* `shell` {Boolean|String} If `true`, runs `command` inside of a shell. Uses
590+
'/bin/sh' on UNIX, and 'cmd.exe' on Windows. A different shell can be
591+
specified as a string. The shell should understand the `-c` switch on UNIX,
592+
or `/s /c` on Windows. Defaults to `false` (no shell).
584593
* return: {Object}
585594
* `pid` {Number} Pid of the child process
586595
* `output` {Array} Array of results from stdio output

lib/child_process.js

+29-24
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ exports._forkChild = function(fd) {
7171

7272

7373
function normalizeExecArgs(command /*, options, callback*/) {
74-
var file, args, options, callback;
74+
let options;
75+
let callback;
7576

7677
if (typeof arguments[1] === 'function') {
7778
options = undefined;
@@ -81,25 +82,12 @@ function normalizeExecArgs(command /*, options, callback*/) {
8182
callback = arguments[2];
8283
}
8384

84-
if (process.platform === 'win32') {
85-
file = process.env.comspec || 'cmd.exe';
86-
args = ['/s', '/c', '"' + command + '"'];
87-
// Make a shallow copy before patching so we don't clobber the user's
88-
// options object.
89-
options = util._extend({}, options);
90-
options.windowsVerbatimArguments = true;
91-
} else {
92-
file = '/bin/sh';
93-
args = ['-c', command];
94-
}
95-
96-
if (options && options.shell)
97-
file = options.shell;
85+
// Make a shallow copy so we don't clobber the user's options object.
86+
options = Object.assign({}, options);
87+
options.shell = typeof options.shell === 'string' ? options.shell : true;
9888

9989
return {
100-
cmd: command,
101-
file: file,
102-
args: args,
90+
file: command,
10391
options: options,
10492
callback: callback
10593
};
@@ -109,7 +97,6 @@ function normalizeExecArgs(command /*, options, callback*/) {
10997
exports.exec = function(command /*, options, callback*/) {
11098
var opts = normalizeExecArgs.apply(null, arguments);
11199
return exports.execFile(opts.file,
112-
opts.args,
113100
opts.options,
114101
opts.callback);
115102
};
@@ -123,7 +110,8 @@ exports.execFile = function(file /*, args, options, callback*/) {
123110
maxBuffer: 200 * 1024,
124111
killSignal: 'SIGTERM',
125112
cwd: null,
126-
env: null
113+
env: null,
114+
shell: false
127115
};
128116

129117
// Parse the optional positional parameters.
@@ -153,6 +141,7 @@ exports.execFile = function(file /*, args, options, callback*/) {
153141
env: options.env,
154142
gid: options.gid,
155143
uid: options.uid,
144+
shell: options.shell,
156145
windowsVerbatimArguments: !!options.windowsVerbatimArguments
157146
});
158147

@@ -331,7 +320,23 @@ function normalizeSpawnArguments(file /*, args, options*/) {
331320
else if (options === null || typeof options !== 'object')
332321
throw new TypeError('"options" argument must be an object');
333322

334-
options = util._extend({}, options);
323+
// Make a shallow copy so we don't clobber the user's options object.
324+
options = Object.assign({}, options);
325+
326+
if (options.shell) {
327+
const command = [file].concat(args).join(' ');
328+
329+
if (process.platform === 'win32') {
330+
file = typeof options.shell === 'string' ? options.shell :
331+
process.env.comspec || 'cmd.exe';
332+
args = ['/s', '/c', '"' + command + '"'];
333+
options.windowsVerbatimArguments = true;
334+
} else {
335+
file = typeof options.shell === 'string' ? options.shell : '/bin/sh';
336+
args = ['-c', command];
337+
}
338+
}
339+
335340
args.unshift(file);
336341

337342
var env = options.env || process.env;
@@ -491,12 +496,12 @@ function execFileSync(/*command, args, options*/) {
491496
exports.execFileSync = execFileSync;
492497

493498

494-
function execSync(/*command, options*/) {
499+
function execSync(command /*, options*/) {
495500
var opts = normalizeExecArgs.apply(null, arguments);
496501
var inheritStderr = opts.options ? !opts.options.stdio : true;
497502

498-
var ret = spawnSync(opts.file, opts.args, opts.options);
499-
ret.cmd = opts.cmd;
503+
var ret = spawnSync(opts.file, opts.options);
504+
ret.cmd = command;
500505

501506
if (inheritStderr)
502507
process.stderr.write(ret.stderr);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const cp = require('child_process');
5+
6+
// Verify that a shell is, in fact, executed
7+
const doesNotExist = cp.spawn('does-not-exist', {shell: true});
8+
9+
assert.notEqual(doesNotExist.spawnfile, 'does-not-exist');
10+
doesNotExist.on('error', common.fail);
11+
doesNotExist.on('exit', common.mustCall((code, signal) => {
12+
assert.strictEqual(signal, null);
13+
14+
if (common.isWindows)
15+
assert.strictEqual(code, 1); // Exit code of cmd.exe
16+
else
17+
assert.strictEqual(code, 127); // Exit code of /bin/sh
18+
}));
19+
20+
// Verify that passing arguments works
21+
const echo = cp.spawn('echo', ['foo'], {
22+
encoding: 'utf8',
23+
shell: true
24+
});
25+
let echoOutput = '';
26+
27+
assert.strictEqual(echo.spawnargs[echo.spawnargs.length - 1].replace(/"/g, ''),
28+
'echo foo');
29+
echo.stdout.on('data', data => {
30+
echoOutput += data;
31+
});
32+
echo.on('close', common.mustCall((code, signal) => {
33+
assert.strictEqual(echoOutput.trim(), 'foo');
34+
}));
35+
36+
// Verify that shell features can be used
37+
const cmd = common.isWindows ? 'echo bar | more' : 'echo bar | cat';
38+
const command = cp.spawn(cmd, {
39+
encoding: 'utf8',
40+
shell: true
41+
});
42+
let commandOutput = '';
43+
44+
command.stdout.on('data', data => {
45+
commandOutput += data;
46+
});
47+
command.on('close', common.mustCall((code, signal) => {
48+
assert.strictEqual(commandOutput.trim(), 'bar');
49+
}));
50+
51+
// Verify that the environment is properly inherited
52+
const env = cp.spawn(`"${process.execPath}" -pe process.env.BAZ`, {
53+
env: Object.assign({}, process.env, {BAZ: 'buzz'}),
54+
encoding: 'utf8',
55+
shell: true
56+
});
57+
let envOutput = '';
58+
59+
env.stdout.on('data', data => {
60+
envOutput += data;
61+
});
62+
env.on('close', common.mustCall((code, signal) => {
63+
assert.strictEqual(envOutput.trim(), 'buzz');
64+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const cp = require('child_process');
5+
6+
// Verify that a shell is, in fact, executed
7+
const doesNotExist = cp.spawnSync('does-not-exist', {shell: true});
8+
9+
assert.notEqual(doesNotExist.file, 'does-not-exist');
10+
assert.strictEqual(doesNotExist.error, undefined);
11+
assert.strictEqual(doesNotExist.signal, null);
12+
13+
if (common.isWindows)
14+
assert.strictEqual(doesNotExist.status, 1); // Exit code of cmd.exe
15+
else
16+
assert.strictEqual(doesNotExist.status, 127); // Exit code of /bin/sh
17+
18+
// Verify that passing arguments works
19+
const echo = cp.spawnSync('echo', ['foo'], {shell: true});
20+
21+
assert.strictEqual(echo.args[echo.args.length - 1].replace(/"/g, ''),
22+
'echo foo');
23+
assert.strictEqual(echo.stdout.toString().trim(), 'foo');
24+
25+
// Verify that shell features can be used
26+
const cmd = common.isWindows ? 'echo bar | more' : 'echo bar | cat';
27+
const command = cp.spawnSync(cmd, {shell: true});
28+
29+
assert.strictEqual(command.stdout.toString().trim(), 'bar');
30+
31+
// Verify that the environment is properly inherited
32+
const env = cp.spawnSync(`"${process.execPath}" -pe process.env.BAZ`, {
33+
env: Object.assign({}, process.env, {BAZ: 'buzz'}),
34+
shell: true
35+
});
36+
37+
assert.strictEqual(env.stdout.toString().trim(), 'buzz');

0 commit comments

Comments
 (0)