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: https://github.com/nodejs/node/issues/1009 PR-URL: https://github.com/nodejs/node/pull/4598 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
parent
34daaa764f
commit
c3bb4b1aa5
@ -77,11 +77,12 @@ The importance of the distinction between `child_process.exec()` and
|
|||||||
`child_process.execFile()` can vary based on platform. On Unix-type operating
|
`child_process.execFile()` can vary based on platform. On Unix-type operating
|
||||||
systems (Unix, Linux, OSX) `child_process.execFile()` can be more efficient
|
systems (Unix, Linux, OSX) `child_process.execFile()` can be more efficient
|
||||||
because it does not spawn a shell. On Windows, however, `.bat` and `.cmd`
|
because it does not spawn a shell. On Windows, however, `.bat` and `.cmd`
|
||||||
files are not executable on their own without a terminal and therefore cannot
|
files are not executable on their own without a terminal, and therefore cannot
|
||||||
be launched using `child_process.execFile()` (or even `child_process.spawn()`).
|
be launched using `child_process.execFile()`. When running on Windows, `.bat`
|
||||||
When running on Windows, `.bat` and `.cmd` files can only be invoked using
|
and `.cmd` files can be invoked using `child_process.spawn()` with the `shell`
|
||||||
either `child_process.exec()` or by spawning `cmd.exe` and passing the `.bat`
|
option set, with `child_process.exec()`, or by spawning `cmd.exe` and passing
|
||||||
or `.cmd` file as an argument (which is what `child_process.exec()` does).
|
the `.bat` or `.cmd` file as an argument (which is what the `shell` option and
|
||||||
|
`child_process.exec()` do).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// On Windows Only ...
|
// On Windows Only ...
|
||||||
@ -277,6 +278,10 @@ not clone the current process.*
|
|||||||
[`options.detached`][])
|
[`options.detached`][])
|
||||||
* `uid` {Number} Sets the user identity of the process. (See setuid(2).)
|
* `uid` {Number} Sets the user identity of the process. (See setuid(2).)
|
||||||
* `gid` {Number} Sets the group identity of the process. (See setgid(2).)
|
* `gid` {Number} Sets the group identity of the process. (See setgid(2).)
|
||||||
|
* `shell` {Boolean|String} If `true`, runs `command` inside of a shell. Uses
|
||||||
|
'/bin/sh' on UNIX, and 'cmd.exe' on Windows. A different shell can be
|
||||||
|
specified as a string. The shell should understand the `-c` switch on UNIX,
|
||||||
|
or `/s /c` on Windows. Defaults to `false` (no shell).
|
||||||
* return: {ChildProcess object}
|
* return: {ChildProcess object}
|
||||||
|
|
||||||
The `child_process.spawn()` method spawns a new process using the given
|
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
|
|||||||
* `maxBuffer` {Number} largest amount of data (in bytes) allowed on stdout or
|
* `maxBuffer` {Number} largest amount of data (in bytes) allowed on stdout or
|
||||||
stderr - if exceeded child process is killed
|
stderr - if exceeded child process is killed
|
||||||
* `encoding` {String} The encoding used for all stdio inputs and outputs. (Default: 'buffer')
|
* `encoding` {String} The encoding used for all stdio inputs and outputs. (Default: 'buffer')
|
||||||
|
* `shell` {Boolean|String} If `true`, runs `command` inside of a shell. Uses
|
||||||
|
'/bin/sh' on UNIX, and 'cmd.exe' on Windows. A different shell can be
|
||||||
|
specified as a string. The shell should understand the `-c` switch on UNIX,
|
||||||
|
or `/s /c` on Windows. Defaults to `false` (no shell).
|
||||||
* return: {Object}
|
* return: {Object}
|
||||||
* `pid` {Number} Pid of the child process
|
* `pid` {Number} Pid of the child process
|
||||||
* `output` {Array} Array of results from stdio output
|
* `output` {Array} Array of results from stdio output
|
||||||
|
@ -71,7 +71,8 @@ exports._forkChild = function(fd) {
|
|||||||
|
|
||||||
|
|
||||||
function normalizeExecArgs(command /*, options, callback*/) {
|
function normalizeExecArgs(command /*, options, callback*/) {
|
||||||
var file, args, options, callback;
|
let options;
|
||||||
|
let callback;
|
||||||
|
|
||||||
if (typeof arguments[1] === 'function') {
|
if (typeof arguments[1] === 'function') {
|
||||||
options = undefined;
|
options = undefined;
|
||||||
@ -81,25 +82,12 @@ function normalizeExecArgs(command /*, options, callback*/) {
|
|||||||
callback = arguments[2];
|
callback = arguments[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
// Make a shallow copy so we don't clobber the user's options object.
|
||||||
file = process.env.comspec || 'cmd.exe';
|
options = Object.assign({}, options);
|
||||||
args = ['/s', '/c', '"' + command + '"'];
|
options.shell = typeof options.shell === 'string' ? options.shell : true;
|
||||||
// Make a shallow copy before patching so we don't clobber the user's
|
|
||||||
// options object.
|
|
||||||
options = util._extend({}, options);
|
|
||||||
options.windowsVerbatimArguments = true;
|
|
||||||
} else {
|
|
||||||
file = '/bin/sh';
|
|
||||||
args = ['-c', command];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options && options.shell)
|
|
||||||
file = options.shell;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cmd: command,
|
file: command,
|
||||||
file: file,
|
|
||||||
args: args,
|
|
||||||
options: options,
|
options: options,
|
||||||
callback: callback
|
callback: callback
|
||||||
};
|
};
|
||||||
@ -109,7 +97,6 @@ function normalizeExecArgs(command /*, options, callback*/) {
|
|||||||
exports.exec = function(command /*, options, callback*/) {
|
exports.exec = function(command /*, options, callback*/) {
|
||||||
var opts = normalizeExecArgs.apply(null, arguments);
|
var opts = normalizeExecArgs.apply(null, arguments);
|
||||||
return exports.execFile(opts.file,
|
return exports.execFile(opts.file,
|
||||||
opts.args,
|
|
||||||
opts.options,
|
opts.options,
|
||||||
opts.callback);
|
opts.callback);
|
||||||
};
|
};
|
||||||
@ -123,7 +110,8 @@ exports.execFile = function(file /*, args, options, callback*/) {
|
|||||||
maxBuffer: 200 * 1024,
|
maxBuffer: 200 * 1024,
|
||||||
killSignal: 'SIGTERM',
|
killSignal: 'SIGTERM',
|
||||||
cwd: null,
|
cwd: null,
|
||||||
env: null
|
env: null,
|
||||||
|
shell: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse the optional positional parameters.
|
// Parse the optional positional parameters.
|
||||||
@ -153,6 +141,7 @@ exports.execFile = function(file /*, args, options, callback*/) {
|
|||||||
env: options.env,
|
env: options.env,
|
||||||
gid: options.gid,
|
gid: options.gid,
|
||||||
uid: options.uid,
|
uid: options.uid,
|
||||||
|
shell: options.shell,
|
||||||
windowsVerbatimArguments: !!options.windowsVerbatimArguments
|
windowsVerbatimArguments: !!options.windowsVerbatimArguments
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -331,7 +320,23 @@ function normalizeSpawnArguments(file /*, args, options*/) {
|
|||||||
else if (options === null || typeof options !== 'object')
|
else if (options === null || typeof options !== 'object')
|
||||||
throw new TypeError('"options" argument must be an object');
|
throw new TypeError('"options" argument must be an object');
|
||||||
|
|
||||||
options = util._extend({}, options);
|
// Make a shallow copy so we don't clobber the user's options object.
|
||||||
|
options = Object.assign({}, options);
|
||||||
|
|
||||||
|
if (options.shell) {
|
||||||
|
const command = [file].concat(args).join(' ');
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
file = typeof options.shell === 'string' ? options.shell :
|
||||||
|
process.env.comspec || 'cmd.exe';
|
||||||
|
args = ['/s', '/c', '"' + command + '"'];
|
||||||
|
options.windowsVerbatimArguments = true;
|
||||||
|
} else {
|
||||||
|
file = typeof options.shell === 'string' ? options.shell : '/bin/sh';
|
||||||
|
args = ['-c', command];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args.unshift(file);
|
args.unshift(file);
|
||||||
|
|
||||||
var env = options.env || process.env;
|
var env = options.env || process.env;
|
||||||
@ -491,12 +496,12 @@ function execFileSync(/*command, args, options*/) {
|
|||||||
exports.execFileSync = execFileSync;
|
exports.execFileSync = execFileSync;
|
||||||
|
|
||||||
|
|
||||||
function execSync(/*command, options*/) {
|
function execSync(command /*, options*/) {
|
||||||
var opts = normalizeExecArgs.apply(null, arguments);
|
var opts = normalizeExecArgs.apply(null, arguments);
|
||||||
var inheritStderr = opts.options ? !opts.options.stdio : true;
|
var inheritStderr = opts.options ? !opts.options.stdio : true;
|
||||||
|
|
||||||
var ret = spawnSync(opts.file, opts.args, opts.options);
|
var ret = spawnSync(opts.file, opts.options);
|
||||||
ret.cmd = opts.cmd;
|
ret.cmd = command;
|
||||||
|
|
||||||
if (inheritStderr)
|
if (inheritStderr)
|
||||||
process.stderr.write(ret.stderr);
|
process.stderr.write(ret.stderr);
|
||||||
|
64
test/parallel/test-child-process-spawn-shell.js
Normal file
64
test/parallel/test-child-process-spawn-shell.js
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const cp = require('child_process');
|
||||||
|
|
||||||
|
// Verify that a shell is, in fact, executed
|
||||||
|
const doesNotExist = cp.spawn('does-not-exist', {shell: true});
|
||||||
|
|
||||||
|
assert.notEqual(doesNotExist.spawnfile, 'does-not-exist');
|
||||||
|
doesNotExist.on('error', common.fail);
|
||||||
|
doesNotExist.on('exit', common.mustCall((code, signal) => {
|
||||||
|
assert.strictEqual(signal, null);
|
||||||
|
|
||||||
|
if (common.isWindows)
|
||||||
|
assert.strictEqual(code, 1); // Exit code of cmd.exe
|
||||||
|
else
|
||||||
|
assert.strictEqual(code, 127); // Exit code of /bin/sh
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify that passing arguments works
|
||||||
|
const echo = cp.spawn('echo', ['foo'], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
let echoOutput = '';
|
||||||
|
|
||||||
|
assert.strictEqual(echo.spawnargs[echo.spawnargs.length - 1].replace(/"/g, ''),
|
||||||
|
'echo foo');
|
||||||
|
echo.stdout.on('data', data => {
|
||||||
|
echoOutput += data;
|
||||||
|
});
|
||||||
|
echo.on('close', common.mustCall((code, signal) => {
|
||||||
|
assert.strictEqual(echoOutput.trim(), 'foo');
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify that shell features can be used
|
||||||
|
const cmd = common.isWindows ? 'echo bar | more' : 'echo bar | cat';
|
||||||
|
const command = cp.spawn(cmd, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
let commandOutput = '';
|
||||||
|
|
||||||
|
command.stdout.on('data', data => {
|
||||||
|
commandOutput += data;
|
||||||
|
});
|
||||||
|
command.on('close', common.mustCall((code, signal) => {
|
||||||
|
assert.strictEqual(commandOutput.trim(), 'bar');
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Verify that the environment is properly inherited
|
||||||
|
const env = cp.spawn(`"${process.execPath}" -pe process.env.BAZ`, {
|
||||||
|
env: Object.assign({}, process.env, {BAZ: 'buzz'}),
|
||||||
|
encoding: 'utf8',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
let envOutput = '';
|
||||||
|
|
||||||
|
env.stdout.on('data', data => {
|
||||||
|
envOutput += data;
|
||||||
|
});
|
||||||
|
env.on('close', common.mustCall((code, signal) => {
|
||||||
|
assert.strictEqual(envOutput.trim(), 'buzz');
|
||||||
|
}));
|
37
test/parallel/test-child-process-spawnsync-shell.js
Normal file
37
test/parallel/test-child-process-spawnsync-shell.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const assert = require('assert');
|
||||||
|
const cp = require('child_process');
|
||||||
|
|
||||||
|
// Verify that a shell is, in fact, executed
|
||||||
|
const doesNotExist = cp.spawnSync('does-not-exist', {shell: true});
|
||||||
|
|
||||||
|
assert.notEqual(doesNotExist.file, 'does-not-exist');
|
||||||
|
assert.strictEqual(doesNotExist.error, undefined);
|
||||||
|
assert.strictEqual(doesNotExist.signal, null);
|
||||||
|
|
||||||
|
if (common.isWindows)
|
||||||
|
assert.strictEqual(doesNotExist.status, 1); // Exit code of cmd.exe
|
||||||
|
else
|
||||||
|
assert.strictEqual(doesNotExist.status, 127); // Exit code of /bin/sh
|
||||||
|
|
||||||
|
// Verify that passing arguments works
|
||||||
|
const echo = cp.spawnSync('echo', ['foo'], {shell: true});
|
||||||
|
|
||||||
|
assert.strictEqual(echo.args[echo.args.length - 1].replace(/"/g, ''),
|
||||||
|
'echo foo');
|
||||||
|
assert.strictEqual(echo.stdout.toString().trim(), 'foo');
|
||||||
|
|
||||||
|
// Verify that shell features can be used
|
||||||
|
const cmd = common.isWindows ? 'echo bar | more' : 'echo bar | cat';
|
||||||
|
const command = cp.spawnSync(cmd, {shell: true});
|
||||||
|
|
||||||
|
assert.strictEqual(command.stdout.toString().trim(), 'bar');
|
||||||
|
|
||||||
|
// Verify that the environment is properly inherited
|
||||||
|
const env = cp.spawnSync(`"${process.execPath}" -pe process.env.BAZ`, {
|
||||||
|
env: Object.assign({}, process.env, {BAZ: 'buzz'}),
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(env.stdout.toString().trim(), 'buzz');
|
Loading…
x
Reference in New Issue
Block a user