diff --git a/doc/api/child_process.md b/doc/api/child_process.md index 5b2b16f253f7f8..9cee76c02ea0f9 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -415,7 +415,7 @@ changes: [Default Windows Shell][]. **Default:** `false` (no shell). * `windowsVerbatimArguments` {boolean} No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to `true` automatically - when `shell` is specified. **Default:** `false`. + when `shell` is specified and is CMD. **Default:** `false`. * `windowsHide` {boolean} Hide the subprocess console window that would normally be created on Windows systems. **Default:** `true`. * Returns: {ChildProcess} @@ -867,7 +867,7 @@ changes: [Default Windows Shell][]. **Default:** `false` (no shell). * `windowsVerbatimArguments` {boolean} No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to `true` automatically - when `shell` is specified. **Default:** `false`. + when `shell` is specified and is CMD. **Default:** `false`. * `windowsHide` {boolean} Hide the subprocess console window that would normally be created on Windows systems. **Default:** `true`. * Returns: {Object} @@ -1432,8 +1432,9 @@ to `stdout` although there are only 4 characters. ## Shell Requirements -The shell should understand the `-c` switch on UNIX or `/d /s /c` on Windows. -On Windows, command line parsing should be compatible with `'cmd.exe'`. +The shell should understand the `-c` switch. If the shell is `'cmd.exe'`, it +should understand the `/d /s /c` switches and command line parsing should be +compatible. ## Default Windows Shell diff --git a/lib/child_process.js b/lib/child_process.js index 2e9c71c1ef0a5e..2962f5cf0599af 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -469,14 +469,19 @@ function normalizeSpawnArguments(file, args, options) { if (options.shell) { const command = [file].concat(args).join(' '); - + // Set the shell, switches, and commands. if (process.platform === 'win32') { if (typeof options.shell === 'string') file = options.shell; else file = process.env.comspec || 'cmd.exe'; - args = ['/d', '/s', '/c', `"${command}"`]; - options.windowsVerbatimArguments = true; + // '/d /s /c' is used only for cmd.exe. + if (/^(?:.*\\)?cmd(?:\.exe)?$/i.test(file)) { + args = ['/d', '/s', '/c', `"${command}"`]; + options.windowsVerbatimArguments = true; + } else { + args = ['-c', command]; + } } else { if (typeof options.shell === 'string') file = options.shell; diff --git a/test/parallel/test-child-process-exec-any-shells-windows.js b/test/parallel/test-child-process-exec-any-shells-windows.js new file mode 100644 index 00000000000000..8cdd03d7e510d5 --- /dev/null +++ b/test/parallel/test-child-process-exec-any-shells-windows.js @@ -0,0 +1,68 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const cp = require('child_process'); +const fs = require('fs'); +const tmpdir = require('../common/tmpdir'); + +// This test is only relevant on Windows. +if (!common.isWindows) + common.skip('Windows specific test.'); + +// This test ensures that child_process.exec can work with any shells. + +tmpdir.refresh(); +const tmpPath = `${tmpdir.path}\\path with spaces`; +fs.mkdirSync(tmpPath); + +const test = (shell) => { + cp.exec('echo foo bar', { shell: shell }, + common.mustCall((error, stdout, stderror) => { + assert.ok(!error && !stderror); + assert.ok(stdout.includes('foo') && stdout.includes('bar')); + })); +}; +const testCopy = (shellName, shellPath) => { + // Copy the executable to a path with spaces, to ensure there are no issues + // related to quoting of argv0 + const copyPath = `${tmpPath}\\${shellName}`; + fs.copyFileSync(shellPath, copyPath); + test(copyPath); +}; + +const system32 = `${process.env.SystemRoot}\\System32`; + +// Test CMD +test(true); +test('cmd'); +testCopy('cmd.exe', `${system32}\\cmd.exe`); +test('cmd.exe'); +test('CMD'); + +// Test PowerShell +test('powershell'); +testCopy('powershell.exe', + `${system32}\\WindowsPowerShell\\v1.0\\powershell.exe`); +fs.writeFile(`${tmpPath}\\test file`, 'Test', common.mustCall((err) => { + assert.ifError(err); + cp.exec(`Get-ChildItem "${tmpPath}" | Select-Object -Property Name`, + { shell: 'PowerShell' }, + common.mustCall((error, stdout, stderror) => { + assert.ok(!error && !stderror); + assert.ok(stdout.includes( + 'test file')); + })); +})); + +// Test Bash (from WSL and Git), if available +cp.exec('where bash', common.mustCall((error, stdout) => { + if (error) { + return; + } + const lines = stdout.trim().split(/[\r\n]+/g); + for (let i = 0; i < lines.length; ++i) { + const bashPath = lines[i].trim(); + test(bashPath); + testCopy(`bash_${i}.exe`, bashPath); + } +})); diff --git a/test/parallel/test-child-process-spawnsync-shell.js b/test/parallel/test-child-process-spawnsync-shell.js index 637cb12a2332bd..6f7c0ec0f14a26 100644 --- a/test/parallel/test-child-process-spawnsync-shell.js +++ b/test/parallel/test-child-process-spawnsync-shell.js @@ -54,11 +54,12 @@ assert.strictEqual(env.stdout.toString().trim(), 'buzz'); function test(testPlatform, shell, shellOutput) { platform = testPlatform; - + const isCmd = /^(?:.*\\)?cmd(?:\.exe)?$/i.test(shellOutput); const cmd = 'not_a_real_command'; - const shellFlags = platform === 'win32' ? ['/d', '/s', '/c'] : ['-c']; - const outputCmd = platform === 'win32' ? `"${cmd}"` : cmd; - const windowsVerbatim = platform === 'win32' ? true : undefined; + + const shellFlags = isCmd ? ['/d', '/s', '/c'] : ['-c']; + const outputCmd = isCmd ? `"${cmd}"` : cmd; + const windowsVerbatim = isCmd ? true : undefined; internalCp.spawnSync = common.mustCall(function(opts) { assert.strictEqual(opts.file, shellOutput); assert.deepStrictEqual(opts.args,