From ba9d9f390ec3fd07536d216f3ecccb8b617fc0af Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Thu, 12 Sep 2019 13:53:19 +0800 Subject: [PATCH 01/20] child_process: escape args[] for shell BREAKING CHANGE: This changes the behavior of args[] in `shell: true` to escape globs and other metacharacters. If you want to keep your scripts, do it the proper way and stuff them in "command." Refs: https://github.com/nodejs/node/pull/29576 Fixes: https://github.com/nodejs/node/issues/29532 --- doc/api/child_process.md | 73 ++++++++++++++--- lib/child_process.js | 77 ++++++++++++------ .../test-child-process-spawn-shell.js | 2 +- ...test-child-process-spawnsync-shell-args.js | 79 +++++++++++++++++++ .../test-child-process-spawnsync-shell.js | 2 +- 5 files changed, 200 insertions(+), 33 deletions(-) create mode 100644 test/parallel/test-child-process-spawnsync-shell-args.js diff --git a/doc/api/child_process.md b/doc/api/child_process.md index afd756db79ec..1f70c7913895 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -469,8 +469,10 @@ changes: See [Advanced serialization][] for more details. **Default:** `'json'`. * `shell` {boolean|string} If `true`, runs `command` inside of a shell. Uses `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different - shell can be specified as a string. See [Shell requirements][] and - [Default Windows shell][]. **Default:** `false` (no shell). + shell can be specified as a string. Unlike `command`, `args` will be quoted + and no metacharacters will be effective inside of it. See + [Shell Requirements][] and [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 and is CMD. **Default:** `false`. @@ -818,8 +820,10 @@ changes: normally be created on Windows systems. **Default:** `false`. * `shell` {boolean|string} If `true`, runs `command` inside of a shell. Uses `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different - shell can be specified as a string. See [Shell requirements][] and - [Default Windows shell][]. **Default:** `false` (no shell). + shell can be specified as a string. Unlike `command`, `args` will be quoted + and no metacharacters will be effective inside of it. See + [Shell Requirements][] and [Default Windows Shell][]. **Default:** `false` + (no shell). * Returns: {Buffer|string} The stdout from the command. The `child_process.execFileSync()` method is generally identical to @@ -949,8 +953,10 @@ changes: **Default:** `'buffer'`. * `shell` {boolean|string} If `true`, runs `command` inside of a shell. Uses `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different - shell can be specified as a string. See [Shell requirements][] and - [Default Windows shell][]. **Default:** `false` (no shell). + shell can be specified as a string. Unlike `command`, `args` will be quoted + and no metacharacters will be effective inside of it. See + [Shell Requirements][] and [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 and is CMD. **Default:** `false`. @@ -1640,7 +1646,35 @@ 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 +If `shell` and `args` are both specified, the shell should understand +[POSIX single-quotes][]. If the shell is `'cmd.exe'`, it should understand the +double-quoting used by Windows `cmd`. If the shell is `'powershell.exe'` or +`'pwsh'`, it should understand a powershell-compatible single quoting. + +Since the `command` part is literally passed to the shell, you will be +responsible for any escaping you perform. The specific way to call a file +as a command-line program depends on the shell: + +* With POSIX shell, you quote the path using the usual single-quotes. You + may want to prefix it with `command --` or `env --` to suppress syntax, + function, and/or alias lookups. You can simply use `'command --'` as your + `command` argument and put the path on the first element of `args`. +* With PowerShell, it is often needed to prefix the command with `&` when the + executable path is quoted. You can use a similar technique with `command` + set to `'&'`. +* With CMD, you should wrap double quotes around the paths to the executable. + Do NOT escape anything inside of it. + +As a caveat for `'cmd'`, `args` should additionally not contain any newlines +because `cmd` cannot handle such. See also [Windows Command Line][] for when +else not to rely on the builtin escaping mechanism on Windows. + +In addition, PowerShell has an [ongoing, cross-platform issue][] with how it +translates the `argv` for external programs to a Windows-style command-line +string. Until it is fixed, the built-in PowerShell escape provided by Node.js +is guaranteed to work for built-in commands only. + +## Default Windows Shell Although Microsoft specifies `%COMSPEC%` must contain the path to `'cmd.exe'` in the root environment, child processes are not always subject to @@ -1648,7 +1682,21 @@ the same requirement. Thus, in `child_process` functions where a shell can be spawned, `'cmd.exe'` is used as a fallback if `process.env.ComSpec` is unavailable. -## Advanced serialization +## Windows Command Line + +There is no universal way in Windows to do command-line escapes as every +program are exposed natively to its full cmdline as a string, and each of them +can have their own parsing rules, which includes optional glob expansion, +officially provided as `_setargv`. + +The most common parsing is defined in the C runtime library as +`CommandLineToArgvW`. Microsoft describes the rules in +[Parsing C++ Command-Line Arguments][]. When any other rules are known to be +used, `windowsVerbatimArguments` should be specified with manual argument +quoting. Since `cmd` has a `/s` switch that allows for verbatim processing +of the command string, we turn on the flag to take advantage of that feature. + +## Advanced Serialization -`shellEscape` is an option introduced in Node.js 1X.Y.Z. It is intended to +`shellEscape` is an option intended to improve the safety of shell command-line by unifying the behavior of `args[]` with regard to metacharacters with the `shell: false` case. All metacharacters and shell-specific scripting should be placed in `command` instead. To preserve @@ -1720,17 +1723,16 @@ function quoteCygwinArg(arg, mayBePath = true) { // passing-in of them. // (There is a Cygwin bug in here about " never being escapable on these // paths:https://cygwin.com/ml/cygwin/2019-10/msg00033.html) - const isPath = mayBePath ? - !!(/^\\\\[a-z][^\\]+\\|^[a-z]:/i.exec(arg)) : - /^[a-z]:/i.exec(arg); + const regex = mayBePath ? /^\\\\[a-z][^\\]+\\|^[a-z]:/i : /^[a-z]:/i; + const isPath = regex.test(arg); if (!isPath) { - arg = arg.replace(/[\\]/g, '\\\\'); - if (/^\\\\[a-z][^\\]+\\/i.exec(arg)) + arg = arg.replaceAll('\\', '\\\\'); + if (/^\\\\[a-z][^\\]+\\/i.test(arg)) // If Cygwin sees this as an SMB network path... // Chop off the first \\ (it seems to not eat up the a in \a) arg = arg.substring(2); } - return `"${arg.replace(/"/g, '""')}"`; + return `"${arg.replaceAll('"', '""')}"`; } // For shell (set { shellEscape: false } for spawn): From 240fddf83baff4f9dfc29f03d7aa1e0aa542d306 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Sun, 25 Oct 2020 14:29:07 +0800 Subject: [PATCH 08/20] child_process.md: link ordering (incomplete) Is there a way to automatically do this? Running lint-md -o rewrites too much. --- doc/api/child_process.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/api/child_process.md b/doc/api/child_process.md index 5d071f33dda0..d82f420dc8f7 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -1717,6 +1717,8 @@ A very common, alternative command-line parsing method is the Cygwin/MSYS2 [`build_argv`][]. It uses always-on globbing based on the POSIX glob(3), with special provisions for dos-like paths. You may escape it as: + ```js function quoteCygwinArg(arg, mayBePath = true) { // Cygwin escapes the backslashes on paths to handle the Windows verbatim @@ -1768,7 +1770,12 @@ or [`child_process.fork()`][]. [Advanced serialization]: #child_process_advanced_serialization [Default Windows shell]: #child_process_default_windows_shell [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +[`build_argv`]: https://github.com/mirror/newlib-cygwin/blob/b39cd00/winsup/cygwin/dcrt0.cc#L292 [Shell requirements]: #child_process_shell_requirements +[ongoing, cross-platform issue]: https://github.com/PowerShell/PowerShell/issues/1995 +[`options.detached`]: #child_process_options_detached +[POSIX single-quotes]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_02 +[Parsing C++ Command-Line Arguments]: https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments [`'disconnect'`]: process.md#process_event_disconnect [`'error'`]: #child_process_event_error [`'exit'`]: #child_process_event_exit @@ -1786,7 +1793,6 @@ or [`child_process.fork()`][]. [`maxBuffer` and Unicode]: #child_process_maxbuffer_and_unicode [`net.Server`]: net.md#net_class_net_server [`net.Socket`]: net.md#net_class_net_socket -[`options.detached`]: #child_process_options_detached [`process.disconnect()`]: process.md#process_process_disconnect [`process.env`]: process.md#process_process_env [`process.execPath`]: process.md#process_process_execpath @@ -1800,14 +1806,7 @@ or [`child_process.fork()`][]. [`subprocess.stdin`]: #child_process_subprocess_stdin [`subprocess.stdio`]: #child_process_subprocess_stdio [`subprocess.stdout`]: #child_process_subprocess_stdout -[`util.promisify()`]: util.html#util_util_promisify_original -[Default Windows Shell]: #child_process_default_windows_shell -[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm -[Shell Requirements]: #child_process_shell_requirements -[POSIX single-quotes]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_02 -[Parsing C++ Command-Line Arguments]: https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments +[`util.promisify()`]: util.md#util_util_promisify_original [Windows Command Line]: #child_process_windows_command_line -[ongoing, cross-platform issue]: https://github.com/PowerShell/PowerShell/issues/1995 -[`build_argv`]: https://github.com/mirror/newlib-cygwin/blob/b39cd00/winsup/cygwin/dcrt0.cc#L292 [synchronous counterparts]: #child_process_synchronous_process_creation [v8.serdes]: v8.md#v8_serialization_api From a0c791a2c3f55f6dee340ae9edc1264986fec766 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Sun, 25 Oct 2020 14:31:40 +0800 Subject: [PATCH 09/20] js: fixup for suggestion --- lib/child_process.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/child_process.js b/lib/child_process.js index b3e41e638997..282929d3205b 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -27,6 +27,7 @@ const { ArrayPrototypeIncludes, ArrayPrototypeJoin, ArrayPrototypeLastIndexOf, + ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSort, From 551417b95e96ac70074e240237ae6a5e80859280 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Sat, 7 Nov 2020 19:57:29 +0800 Subject: [PATCH 10/20] really fix the link ordering --- doc/api/child_process.md | 10 +++++----- tools/doc/checkLinks.js | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/api/child_process.md b/doc/api/child_process.md index d82f420dc8f7..f1a42095107e 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -1770,12 +1770,10 @@ or [`child_process.fork()`][]. [Advanced serialization]: #child_process_advanced_serialization [Default Windows shell]: #child_process_default_windows_shell [HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm -[`build_argv`]: https://github.com/mirror/newlib-cygwin/blob/b39cd00/winsup/cygwin/dcrt0.cc#L292 -[Shell requirements]: #child_process_shell_requirements -[ongoing, cross-platform issue]: https://github.com/PowerShell/PowerShell/issues/1995 -[`options.detached`]: #child_process_options_detached [POSIX single-quotes]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02_02 [Parsing C++ Command-Line Arguments]: https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments +[Shell requirements]: #child_process_shell_requirements +[Windows Command Line]: #child_process_windows_command_line [`'disconnect'`]: process.md#process_event_disconnect [`'error'`]: #child_process_event_error [`'exit'`]: #child_process_event_exit @@ -1783,6 +1781,7 @@ or [`child_process.fork()`][]. [`ChildProcess`]: #child_process_class_childprocess [`Error`]: errors.md#errors_class_error [`EventEmitter`]: events.md#events_class_eventemitter +[`build_argv`]: https://github.com/mirror/newlib-cygwin/blob/b39cd00/winsup/cygwin/dcrt0.cc#L292 [`child_process.exec()`]: #child_process_child_process_exec_command_options_callback [`child_process.execFile()`]: #child_process_child_process_execfile_file_args_options_callback [`child_process.execFileSync()`]: #child_process_child_process_execfilesync_file_args_options @@ -1793,6 +1792,7 @@ or [`child_process.fork()`][]. [`maxBuffer` and Unicode]: #child_process_maxbuffer_and_unicode [`net.Server`]: net.md#net_class_net_server [`net.Socket`]: net.md#net_class_net_socket +[`options.detached`]: #child_process_options_detached [`process.disconnect()`]: process.md#process_process_disconnect [`process.env`]: process.md#process_process_env [`process.execPath`]: process.md#process_process_execpath @@ -1807,6 +1807,6 @@ or [`child_process.fork()`][]. [`subprocess.stdio`]: #child_process_subprocess_stdio [`subprocess.stdout`]: #child_process_subprocess_stdout [`util.promisify()`]: util.md#util_util_promisify_original -[Windows Command Line]: #child_process_windows_command_line +[ongoing, cross-platform issue]: https://github.com/PowerShell/PowerShell/issues/1995 [synchronous counterparts]: #child_process_synchronous_process_creation [v8.serdes]: v8.md#v8_serialization_api diff --git a/tools/doc/checkLinks.js b/tools/doc/checkLinks.js index cdee7ca600d0..967d5bcc77ae 100644 --- a/tools/doc/checkLinks.js +++ b/tools/doc/checkLinks.js @@ -1,3 +1,7 @@ +// You are probably here because you failed a PR and wants to see if this thing +// has a --fix option. +// It doesn't. But if you have some Unix utilities, you can just feed the last +// lines of your markdown file to `sort`. 'use strict'; const fs = require('fs'); From 68018aa1af01f20c8e473679bb68b4064a2edf34 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Sat, 7 Nov 2020 20:01:30 +0800 Subject: [PATCH 11/20] LINT GOD PLEASE ACCEPT MY OFFERING --- lib/child_process.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/child_process.js b/lib/child_process.js index 282929d3205b..120bcab078bc 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -553,7 +553,8 @@ function normalizeSpawnArguments(file, args, options) { } else if (process.platform === 'win32') { file = process.env.comspec || 'cmd.exe'; testWindowsExe = (cmd, file) => - RegExpPrototypeTest(new RegExp(`^(?:.*\\\\)?${cmd}(?:\\.exe)?$`, 'i'), file); + RegExpPrototypeTest(new RegExp(`^(?:.*\\\\)?${cmd}(?:\\.exe)?$`, 'i'), + file); } else if (process.platform === 'android') { file = '/system/bin/sh'; } else { From d66746c0b5bb7bc10119cb0d8f7f2c7f98892b35 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Mon, 9 Nov 2020 08:25:58 +0800 Subject: [PATCH 12/20] Allow custom escapers --- doc/api/child_process.md | 27 ++++++++++++++++----------- lib/child_process.js | 7 +++++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/doc/api/child_process.md b/doc/api/child_process.md index f1a42095107e..eda5aa310482 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -280,8 +280,9 @@ changes: `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different shell can be specified as a string. See [Shell Requirements][] and [Default Windows Shell][]. **Default:** `false` (no shell). - * `shellEscape` {boolean} If `true`, escape `args[]`, but not `command`, - for the shell selected. **Default:** `false` (no escaping). + * `shellEscape` {boolean|Function} If `true`, escape `args[]`, but not + `command`, for the shell selected. If a function, use it as the escaper. + **Default:** `false` (no escaping). * `signal` {AbortSignal} allows aborting the execFile using an AbortSignal. * `callback` {Function} Called with the output when process terminates. * `error` {Error} @@ -473,8 +474,9 @@ changes: `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different shell can be specified as a string. See [Shell Requirements][] and [Default Windows Shell][]. **Default:** `false` (no shell). - * `shellEscape` {boolean} If `true`, escape `args[]`, but not `command`, - for the shell selected. **Default:** `false` (no escaping). + * `shellEscape` {boolean|Function} If `true`, escape `args[]`, but not + `command`, for the shell selected. If a function, use it as the escaper. + **Default:** `false` (no escaping). * `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 and is CMD. **Default:** `false`. @@ -824,8 +826,9 @@ changes: `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different shell can be specified as a string. See [Shell Requirements][] and [Default Windows Shell][]. **Default:** `false` (no shell). - * `shellEscape` {boolean} If `true`, escape `args[]`, but not `command`, - for the shell selected. **Default:** `false` (no escaping). + * `shellEscape` {boolean|Function} If `true`, escape `args[]`, but not + `command`, for the shell selected. If a function, use it as the escaper. + **Default:** `false` (no escaping). * Returns: {Buffer|string} The stdout from the command. The `child_process.execFileSync()` method is generally identical to @@ -957,8 +960,9 @@ changes: `'/bin/sh'` on Unix, and `process.env.ComSpec` on Windows. A different shell can be specified as a string. See [Shell Requirements][] and [Default Windows Shell][]. **Default:** `false` (no shell). - * `shellEscape` {boolean} If `true`, escape `args[]`, but not `command`, - for the shell selected. **Default:** `false` (no escaping). + * `shellEscape` {boolean|Function} If `true`, escape `args[]`, but not + `command`, for the shell selected. If a function, use it as the escaper. + **Default:** `false` (no escaping). * `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 and is CMD. **Default:** `false`. @@ -1737,10 +1741,11 @@ function quoteCygwinArg(arg, mayBePath = true) { return `"${arg.replaceAll('"', '""')}"`; } -// For shell (set { shellEscape: false } for spawn): -exec([command].concat(args.map(quoteCygwinArg)).join(' ')); // For non-shell: -spawn(command, args.map(quoteCygwinArg), { windowsVerbatimArguments: true }); +spawn(command, args, { + shellEscape: quoteCygwinArg, + windowsVerbatimArguments: true, +}); ``` MinGW uses the native Windows mechanism and can be used safely with the diff --git a/lib/child_process.js b/lib/child_process.js index 120bcab078bc..eea079dab93f 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -513,9 +513,10 @@ function normalizeSpawnArguments(file, args, options) { // Validate shellEscape, if present. if (options.shellEscape != null && - typeof options.shellEscape !== 'boolean') { + typeof options.shellEscape !== 'boolean' && + typeof options.shellEscape !== 'function') { throw new ERR_INVALID_ARG_TYPE('options.shellEscape', - ['boolean'], options.shellEscape); + ['boolean', 'function'], options.shellEscape); } // Validate argv0, if present. @@ -577,6 +578,8 @@ function normalizeSpawnArguments(file, args, options) { // Make it opt-in... for now. if (!options.shellEscape) quote = (s) => s; + else if (typeof options.shellEscape == 'function') + quote = options.shellEscape; // Cmd accepts quoted things verbatim under /s. const commandArg = ArrayPrototypeJoin( From 19c2125ad5c6ca2520e2f6353f115a11f3bda25d Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Mon, 9 Nov 2020 08:28:06 +0800 Subject: [PATCH 13/20] testWindowsExe should always trigger on win32 --- lib/child_process.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/child_process.js b/lib/child_process.js index eea079dab93f..73dd077aa451 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -516,7 +516,8 @@ function normalizeSpawnArguments(file, args, options) { typeof options.shellEscape !== 'boolean' && typeof options.shellEscape !== 'function') { throw new ERR_INVALID_ARG_TYPE('options.shellEscape', - ['boolean', 'function'], options.shellEscape); + ['boolean', 'function'], + options.shellEscape); } // Validate argv0, if present. @@ -547,15 +548,19 @@ function normalizeSpawnArguments(file, args, options) { args = ['-c']; let quote = quotePosixShArg; let isCmd = false; - let testWindowsExe = (_c, _f) => false; + const testWindowsExe = + process.platform === 'win32' ? + (cmd, file) => + RegExpPrototypeTest( + new RegExp(`^(?:.*\\\\)?${cmd}(?:\\.exe)?$`, 'i'), + file + ) : + (_c, _f) => false; // Set the shell. if (typeof options.shell === 'string') { file = options.shell; } else if (process.platform === 'win32') { file = process.env.comspec || 'cmd.exe'; - testWindowsExe = (cmd, file) => - RegExpPrototypeTest(new RegExp(`^(?:.*\\\\)?${cmd}(?:\\.exe)?$`, 'i'), - file); } else if (process.platform === 'android') { file = '/system/bin/sh'; } else { From 1e54c83922cd9e82c8fc33ba771e4474f335254a Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Mon, 9 Nov 2020 08:34:02 +0800 Subject: [PATCH 14/20] doc --- doc/api/child_process.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/child_process.md b/doc/api/child_process.md index eda5aa310482..99539b0a1282 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -1667,7 +1667,7 @@ If all of `shell`, `args`, and `shellEscape` are specified, the shell should understand [POSIX single-quotes][]. If the shell is `'cmd.exe'`, it should understand the double-quoting used by Windows `cmd`. If the shell is `'powershell.exe'` or `'pwsh'`, it should understand a powershell-compatible -single quoting. +single quoting. If all fails, you may provide your own quoting function. Since the `command` part is literally passed to the shell, you are responsible for any escaping you perform. The specific way to call a file as a command-line @@ -1722,7 +1722,7 @@ A very common, alternative command-line parsing method is the Cygwin/MSYS2 with special provisions for dos-like paths. You may escape it as: + It's very annoying to read. --> ```js function quoteCygwinArg(arg, mayBePath = true) { // Cygwin escapes the backslashes on paths to handle the Windows verbatim From 5821e0aa184245a6493503de1acab88e99912cd3 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Fri, 13 Nov 2020 00:19:02 +0800 Subject: [PATCH 15/20] remove irrlevant comment --- tools/doc/checkLinks.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/doc/checkLinks.js b/tools/doc/checkLinks.js index 967d5bcc77ae..cdee7ca600d0 100644 --- a/tools/doc/checkLinks.js +++ b/tools/doc/checkLinks.js @@ -1,7 +1,3 @@ -// You are probably here because you failed a PR and wants to see if this thing -// has a --fix option. -// It doesn't. But if you have some Unix utilities, you can just feed the last -// lines of your markdown file to `sort`. 'use strict'; const fs = require('fs'); From cdae7ffef0cbc175e26acc6fb1d951f6fad443b0 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Fri, 13 Nov 2020 00:26:01 +0800 Subject: [PATCH 16/20] add node for cmd arg --- lib/child_process.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/child_process.js b/lib/child_process.js index 73dd077aa451..8f0a53086c3d 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -834,6 +834,14 @@ function quoteCmdArg(arg) { // // Don't put newlines in here; there is still no way to escape it. ^ before // EOL only does continuation. + // + // As with POSIX, always quoting is simpler and less prone to breakage. + // This also makes behavior consistent wrt globs. + // + // This is written assuming the receiving program is a normal MSVCRT exe. + // Batch files have differences in that cmd double-interprets the string, + // but that should not affect us as the result is simply inert to CMD here. + // cf. https://daviddeley.com/autohotkey/parameters/parameters.htm#BATCH return `"${StringPrototypeReplace( StringPrototypeReplace(arg, /(\\*)($|")/g, '$1$1$2'), /"/g, '""')}"`; } From 770d59a6ed6fe67f44ddab07a966b378197bee44 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Fri, 13 Nov 2020 00:33:16 +0800 Subject: [PATCH 17/20] validate for lack of newline in quoteCmdArg --- lib/child_process.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/child_process.js b/lib/child_process.js index 8f0a53086c3d..33006de0adc2 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -842,6 +842,10 @@ function quoteCmdArg(arg) { // Batch files have differences in that cmd double-interprets the string, // but that should not affect us as the result is simply inert to CMD here. // cf. https://daviddeley.com/autohotkey/parameters/parameters.htm#BATCH + if (arg.indexOf('\n') != -1 || arg.indexOf('\r') != -1) { + throw new ERR_INVALID_ARG_VALUE(`arg`, arg, + 'cannot contain newlines for cmd'); + } return `"${StringPrototypeReplace( StringPrototypeReplace(arg, /(\\*)($|")/g, '$1$1$2'), /"/g, '""')}"`; } From 46d190b59ff691e37cd3a7e7920597c355b31c69 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Mon, 11 Jan 2021 18:58:20 +0800 Subject: [PATCH 18/20] Appease the linter god --- lib/child_process.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/child_process.js b/lib/child_process.js index 33006de0adc2..2c7a9344cbf1 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -842,7 +842,7 @@ function quoteCmdArg(arg) { // Batch files have differences in that cmd double-interprets the string, // but that should not affect us as the result is simply inert to CMD here. // cf. https://daviddeley.com/autohotkey/parameters/parameters.htm#BATCH - if (arg.indexOf('\n') != -1 || arg.indexOf('\r') != -1) { + if (arg.indexOf('\n') !== -1 || arg.indexOf('\r') !== -1) { throw new ERR_INVALID_ARG_VALUE(`arg`, arg, 'cannot contain newlines for cmd'); } From 804a34637502961eb48cfcbab37b0809de8444a0 Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Tue, 12 Jan 2021 18:54:24 +0800 Subject: [PATCH 19/20] fix duplicate import --- lib/child_process.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/child_process.js b/lib/child_process.js index 2c7a9344cbf1..a297d2598ed4 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -39,16 +39,14 @@ const { ObjectDefineProperty, ObjectPrototypeHasOwnProperty, Promise, - RegExpPrototypeTest, - SafeSet, - StringPrototypeSlice, - StringPrototypeToUpperCase, - Set, RegExp, RegExpPrototypeTest, + SafeSet, String, StringPrototypeEndsWith, StringPrototypeReplace, + StringPrototypeSlice, + StringPrototypeToUpperCase, } = primordials; const { From 0dfba452583c2c9ac46aef681f0456fccdd74cbc Mon Sep 17 00:00:00 2001 From: Mingye Wang Date: Fri, 7 May 2021 21:38:49 +0800 Subject: [PATCH 20/20] Apply suggestions from code review Co-authored-by: Antoine du Hamel --- lib/child_process.js | 11 +++++------ test/parallel/test-child-process-spawn-shell.js | 2 +- test/parallel/test-child-process-spawnsync-shell.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/child_process.js b/lib/child_process.js index a297d2598ed4..2639b547b3ab 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -585,10 +585,9 @@ function normalizeSpawnArguments(file, args, options) { quote = options.shellEscape; // Cmd accepts quoted things verbatim under /s. - const commandArg = ArrayPrototypeJoin( - [command, ...ArrayPrototypeMap(commandArgs, quote)], - ' ' - ); + const commandArg = commandArgs.length ? + `${command} ${ArrayPrototypeJoin(ArrayPrototypeMap(commandArgs, quote), ' ')}`: + command; ArrayPrototypePush(args, isCmd ? `"${commandArg}"` : commandArg); } @@ -840,8 +839,8 @@ function quoteCmdArg(arg) { // Batch files have differences in that cmd double-interprets the string, // but that should not affect us as the result is simply inert to CMD here. // cf. https://daviddeley.com/autohotkey/parameters/parameters.htm#BATCH - if (arg.indexOf('\n') !== -1 || arg.indexOf('\r') !== -1) { - throw new ERR_INVALID_ARG_VALUE(`arg`, arg, + if (StringPrototypeIncludes(arg, '\n') || StringPrototypeIncludes(arg, '\r')) { + throw new ERR_INVALID_ARG_VALUE('arg', arg, 'cannot contain newlines for cmd'); } return `"${StringPrototypeReplace( diff --git a/test/parallel/test-child-process-spawn-shell.js b/test/parallel/test-child-process-spawn-shell.js index ca238969cbf8..71df515db6eb 100644 --- a/test/parallel/test-child-process-spawn-shell.js +++ b/test/parallel/test-child-process-spawn-shell.js @@ -24,7 +24,7 @@ const echo = cp.spawn('echo', ['foo'], { }); let echoOutput = ''; -assert.strictEqual(echo.spawnargs[echo.spawnargs.length - 1].replace(/['"]/g, ''), +assert.strictEqual(echo.spawnargs[echo.spawnargs.length - 1].replace(/['"]/g, '', { shellEscape: true }), 'echo foo'); echo.stdout.on('data', (data) => { echoOutput += data; diff --git a/test/parallel/test-child-process-spawnsync-shell.js b/test/parallel/test-child-process-spawnsync-shell.js index 573a9419e991..861b4631cf6d 100644 --- a/test/parallel/test-child-process-spawnsync-shell.js +++ b/test/parallel/test-child-process-spawnsync-shell.js @@ -20,7 +20,7 @@ else // Verify that passing arguments works internalCp.spawnSync = common.mustCall(function(opts) { - assert.strictEqual(opts.args[opts.args.length - 1].replace(/["']/g, ''), + assert.strictEqual(opts.args[opts.args.length - 1].replace(/["']/g, '', { shellEscape: true }), 'echo foo'); return oldSpawnSync(opts); });