Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame^] | 1 | 'use strict'; |
| 2 | |
| 3 | const path = require('path'); |
| 4 | const niceTry = require('nice-try'); |
| 5 | const resolveCommand = require('./util/resolveCommand'); |
| 6 | const escape = require('./util/escape'); |
| 7 | const readShebang = require('./util/readShebang'); |
| 8 | const semver = require('semver'); |
| 9 | |
| 10 | const isWin = process.platform === 'win32'; |
| 11 | const isExecutableRegExp = /\.(?:com|exe)$/i; |
| 12 | const isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; |
| 13 | |
| 14 | // `options.shell` is supported in Node ^4.8.0, ^5.7.0 and >= 6.0.0 |
| 15 | const supportsShellOption = niceTry(() => semver.satisfies(process.version, '^4.8.0 || ^5.7.0 || >= 6.0.0', true)) || false; |
| 16 | |
| 17 | function detectShebang(parsed) { |
| 18 | parsed.file = resolveCommand(parsed); |
| 19 | |
| 20 | const shebang = parsed.file && readShebang(parsed.file); |
| 21 | |
| 22 | if (shebang) { |
| 23 | parsed.args.unshift(parsed.file); |
| 24 | parsed.command = shebang; |
| 25 | |
| 26 | return resolveCommand(parsed); |
| 27 | } |
| 28 | |
| 29 | return parsed.file; |
| 30 | } |
| 31 | |
| 32 | function parseNonShell(parsed) { |
| 33 | if (!isWin) { |
| 34 | return parsed; |
| 35 | } |
| 36 | |
| 37 | // Detect & add support for shebangs |
| 38 | const commandFile = detectShebang(parsed); |
| 39 | |
| 40 | // We don't need a shell if the command filename is an executable |
| 41 | const needsShell = !isExecutableRegExp.test(commandFile); |
| 42 | |
| 43 | // If a shell is required, use cmd.exe and take care of escaping everything correctly |
| 44 | // Note that `forceShell` is an hidden option used only in tests |
| 45 | if (parsed.options.forceShell || needsShell) { |
| 46 | // Need to double escape meta chars if the command is a cmd-shim located in `node_modules/.bin/` |
| 47 | // The cmd-shim simply calls execute the package bin file with NodeJS, proxying any argument |
| 48 | // Because the escape of metachars with ^ gets interpreted when the cmd.exe is first called, |
| 49 | // we need to double escape them |
| 50 | const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); |
| 51 | |
| 52 | // Normalize posix paths into OS compatible paths (e.g.: foo/bar -> foo\bar) |
| 53 | // This is necessary otherwise it will always fail with ENOENT in those cases |
| 54 | parsed.command = path.normalize(parsed.command); |
| 55 | |
| 56 | // Escape command & arguments |
| 57 | parsed.command = escape.command(parsed.command); |
| 58 | parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars)); |
| 59 | |
| 60 | const shellCommand = [parsed.command].concat(parsed.args).join(' '); |
| 61 | |
| 62 | parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; |
| 63 | parsed.command = process.env.comspec || 'cmd.exe'; |
| 64 | parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped |
| 65 | } |
| 66 | |
| 67 | return parsed; |
| 68 | } |
| 69 | |
| 70 | function parseShell(parsed) { |
| 71 | // If node supports the shell option, there's no need to mimic its behavior |
| 72 | if (supportsShellOption) { |
| 73 | return parsed; |
| 74 | } |
| 75 | |
| 76 | // Mimic node shell option |
| 77 | // See https://github.com/nodejs/node/blob/b9f6a2dc059a1062776133f3d4fd848c4da7d150/lib/child_process.js#L335 |
| 78 | const shellCommand = [parsed.command].concat(parsed.args).join(' '); |
| 79 | |
| 80 | if (isWin) { |
| 81 | parsed.command = typeof parsed.options.shell === 'string' ? parsed.options.shell : process.env.comspec || 'cmd.exe'; |
| 82 | parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; |
| 83 | parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped |
| 84 | } else { |
| 85 | if (typeof parsed.options.shell === 'string') { |
| 86 | parsed.command = parsed.options.shell; |
| 87 | } else if (process.platform === 'android') { |
| 88 | parsed.command = '/system/bin/sh'; |
| 89 | } else { |
| 90 | parsed.command = '/bin/sh'; |
| 91 | } |
| 92 | |
| 93 | parsed.args = ['-c', shellCommand]; |
| 94 | } |
| 95 | |
| 96 | return parsed; |
| 97 | } |
| 98 | |
| 99 | function parse(command, args, options) { |
| 100 | // Normalize arguments, similar to nodejs |
| 101 | if (args && !Array.isArray(args)) { |
| 102 | options = args; |
| 103 | args = null; |
| 104 | } |
| 105 | |
| 106 | args = args ? args.slice(0) : []; // Clone array to avoid changing the original |
| 107 | options = Object.assign({}, options); // Clone object to avoid changing the original |
| 108 | |
| 109 | // Build our parsed object |
| 110 | const parsed = { |
| 111 | command, |
| 112 | args, |
| 113 | options, |
| 114 | file: undefined, |
| 115 | original: { |
| 116 | command, |
| 117 | args, |
| 118 | }, |
| 119 | }; |
| 120 | |
| 121 | // Delegate further parsing to shell or non-shell |
| 122 | return options.shell ? parseShell(parsed) : parseNonShell(parsed); |
| 123 | } |
| 124 | |
| 125 | module.exports = parse; |