Skip to content

Commit af883e1

Browse files
tkamenokojoaocgreis
authored andcommitted
child_process: fix switches for alternative shells on Windows
On Windows, normalizeSpawnArguments set "/d /s /c" for any shells. It cause exec and other methods are limited to cmd.exe as a shell. Powershell and git-bash are often used instead of cmd.exe, and they can recieve "-c" switch like unix shells. So normalizeSpawnArguments is changed to set "/d /s /c" for cmd.exe, and "-c" for others. Fixes: #21905 PR-URL: #21943 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: João Reis <reis@janeasystems.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Refael Ackermann <refack@gmail.com>
1 parent 3209679 commit af883e1

4 files changed

+86
-11
lines changed

doc/api/child_process.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ changes:
415415
[Default Windows Shell][]. **Default:** `false` (no shell).
416416
* `windowsVerbatimArguments` {boolean} No quoting or escaping of arguments is
417417
done on Windows. Ignored on Unix. This is set to `true` automatically
418-
when `shell` is specified. **Default:** `false`.
418+
when `shell` is specified and is CMD. **Default:** `false`.
419419
* `windowsHide` {boolean} Hide the subprocess console window that would
420420
normally be created on Windows systems. **Default:** `true`.
421421
* Returns: {ChildProcess}
@@ -867,7 +867,7 @@ changes:
867867
[Default Windows Shell][]. **Default:** `false` (no shell).
868868
* `windowsVerbatimArguments` {boolean} No quoting or escaping of arguments is
869869
done on Windows. Ignored on Unix. This is set to `true` automatically
870-
when `shell` is specified. **Default:** `false`.
870+
when `shell` is specified and is CMD. **Default:** `false`.
871871
* `windowsHide` {boolean} Hide the subprocess console window that would
872872
normally be created on Windows systems. **Default:** `true`.
873873
* Returns: {Object}
@@ -1432,8 +1432,9 @@ to `stdout` although there are only 4 characters.
14321432

14331433
## Shell Requirements
14341434

1435-
The shell should understand the `-c` switch on UNIX or `/d /s /c` on Windows.
1436-
On Windows, command line parsing should be compatible with `'cmd.exe'`.
1435+
The shell should understand the `-c` switch. If the shell is `'cmd.exe'`, it
1436+
should understand the `/d /s /c` switches and command line parsing should be
1437+
compatible.
14371438

14381439
## Default Windows Shell
14391440

lib/child_process.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -469,14 +469,19 @@ function normalizeSpawnArguments(file, args, options) {
469469

470470
if (options.shell) {
471471
const command = [file].concat(args).join(' ');
472-
472+
// Set the shell, switches, and commands.
473473
if (process.platform === 'win32') {
474474
if (typeof options.shell === 'string')
475475
file = options.shell;
476476
else
477477
file = process.env.comspec || 'cmd.exe';
478-
args = ['/d', '/s', '/c', `"${command}"`];
479-
options.windowsVerbatimArguments = true;
478+
// '/d /s /c' is used only for cmd.exe.
479+
if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) {
480+
args = ['/d', '/s', '/c', `"${command}"`];
481+
options.windowsVerbatimArguments = true;
482+
} else {
483+
args = ['-c', command];
484+
}
480485
} else {
481486
if (typeof options.shell === 'string')
482487
file = options.shell;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const cp = require('child_process');
5+
const fs = require('fs');
6+
const tmpdir = require('../common/tmpdir');
7+
8+
// This test is only relevant on Windows.
9+
if (!common.isWindows)
10+
common.skip('Windows specific test.');
11+
12+
// This test ensures that child_process.exec can work with any shells.
13+
14+
tmpdir.refresh();
15+
const tmpPath = `${tmpdir.path}\\path with spaces`;
16+
fs.mkdirSync(tmpPath);
17+
18+
const test = (shell) => {
19+
cp.exec('echo foo bar', { shell: shell },
20+
common.mustCall((error, stdout, stderror) => {
21+
assert.ok(!error && !stderror);
22+
assert.ok(stdout.includes('foo') && stdout.includes('bar'));
23+
}));
24+
};
25+
const testCopy = (shellName, shellPath) => {
26+
// Copy the executable to a path with spaces, to ensure there are no issues
27+
// related to quoting of argv0
28+
const copyPath = `${tmpPath}\\${shellName}`;
29+
fs.copyFileSync(shellPath, copyPath);
30+
test(copyPath);
31+
};
32+
33+
const system32 = `${process.env.SystemRoot}\\System32`;
34+
35+
// Test CMD
36+
test(true);
37+
test('cmd');
38+
testCopy('cmd.exe', `${system32}\\cmd.exe`);
39+
test('cmd.exe');
40+
test('CMD');
41+
42+
// Test PowerShell
43+
test('powershell');
44+
testCopy('powershell.exe',
45+
`${system32}\\WindowsPowerShell\\v1.0\\powershell.exe`);
46+
fs.writeFile(`${tmpPath}\\test file`, 'Test', common.mustCall((err) => {
47+
assert.ifError(err);
48+
cp.exec(`Get-ChildItem "${tmpPath}" | Select-Object -Property Name`,
49+
{ shell: 'PowerShell' },
50+
common.mustCall((error, stdout, stderror) => {
51+
assert.ok(!error && !stderror);
52+
assert.ok(stdout.includes(
53+
'test file'));
54+
}));
55+
}));
56+
57+
// Test Bash (from WSL and Git), if available
58+
cp.exec('where bash', common.mustCall((error, stdout) => {
59+
if (error) {
60+
return;
61+
}
62+
const lines = stdout.trim().split(/[\r\n]+/g);
63+
for (let i = 0; i < lines.length; ++i) {
64+
const bashPath = lines[i].trim();
65+
test(bashPath);
66+
testCopy(`bash_${i}.exe`, bashPath);
67+
}
68+
}));

test/parallel/test-child-process-spawnsync-shell.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,12 @@ assert.strictEqual(env.stdout.toString().trim(), 'buzz');
5454

5555
function test(testPlatform, shell, shellOutput) {
5656
platform = testPlatform;
57-
57+
const isCmd = /^(?:.*\\)?cmd(?:\.exe)?$/i.test(shellOutput);
5858
const cmd = 'not_a_real_command';
59-
const shellFlags = platform === 'win32' ? ['/d', '/s', '/c'] : ['-c'];
60-
const outputCmd = platform === 'win32' ? `"${cmd}"` : cmd;
61-
const windowsVerbatim = platform === 'win32' ? true : undefined;
59+
60+
const shellFlags = isCmd ? ['/d', '/s', '/c'] : ['-c'];
61+
const outputCmd = isCmd ? `"${cmd}"` : cmd;
62+
const windowsVerbatim = isCmd ? true : undefined;
6263
internalCp.spawnSync = common.mustCall(function(opts) {
6364
assert.strictEqual(opts.file, shellOutput);
6465
assert.deepStrictEqual(opts.args,

0 commit comments

Comments
 (0)