| 'use strict'; |
| |
| const path = require('path'); |
| const niceTry = require('nice-try'); |
| const resolveCommand = require('./util/resolveCommand'); |
| const escape = require('./util/escape'); |
| const readShebang = require('./util/readShebang'); |
| const semver = require('semver'); |
| |
| const isWin = process.platform === 'win32'; |
| const isExecutableRegExp = /\.(?:com|exe)$/i; |
| const isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; |
| |
| // `options.shell` is supported in Node ^4.8.0, ^5.7.0 and >= 6.0.0 |
| const supportsShellOption = niceTry(() => semver.satisfies(process.version, '^4.8.0 || ^5.7.0 || >= 6.0.0', true)) || false; |
| |
| function detectShebang(parsed) { |
| parsed.file = resolveCommand(parsed); |
| |
| const shebang = parsed.file && readShebang(parsed.file); |
| |
| if (shebang) { |
| parsed.args.unshift(parsed.file); |
| parsed.command = shebang; |
| |
| return resolveCommand(parsed); |
| } |
| |
| return parsed.file; |
| } |
| |
| function parseNonShell(parsed) { |
| if (!isWin) { |
| return parsed; |
| } |
| |
| // Detect & add support for shebangs |
| const commandFile = detectShebang(parsed); |
| |
| // We don't need a shell if the command filename is an executable |
| const needsShell = !isExecutableRegExp.test(commandFile); |
| |
| // If a shell is required, use cmd.exe and take care of escaping everything correctly |
| // Note that `forceShell` is an hidden option used only in tests |
| if (parsed.options.forceShell || needsShell) { |
| // Need to double escape meta chars if the command is a cmd-shim located in `node_modules/.bin/` |
| // The cmd-shim simply calls execute the package bin file with NodeJS, proxying any argument |
| // Because the escape of metachars with ^ gets interpreted when the cmd.exe is first called, |
| // we need to double escape them |
| const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); |
| |
| // Normalize posix paths into OS compatible paths (e.g.: foo/bar -> foo\bar) |
| // This is necessary otherwise it will always fail with ENOENT in those cases |
| parsed.command = path.normalize(parsed.command); |
| |
| // Escape command & arguments |
| parsed.command = escape.command(parsed.command); |
| parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars)); |
| |
| const shellCommand = [parsed.command].concat(parsed.args).join(' '); |
| |
| parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; |
| parsed.command = process.env.comspec || 'cmd.exe'; |
| parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped |
| } |
| |
| return parsed; |
| } |
| |
| function parseShell(parsed) { |
| // If node supports the shell option, there's no need to mimic its behavior |
| if (supportsShellOption) { |
| return parsed; |
| } |
| |
| // Mimic node shell option |
| // See https://github.com/nodejs/node/blob/b9f6a2dc059a1062776133f3d4fd848c4da7d150/lib/child_process.js#L335 |
| const shellCommand = [parsed.command].concat(parsed.args).join(' '); |
| |
| if (isWin) { |
| parsed.command = typeof parsed.options.shell === 'string' ? parsed.options.shell : process.env.comspec || 'cmd.exe'; |
| parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; |
| parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped |
| } else { |
| if (typeof parsed.options.shell === 'string') { |
| parsed.command = parsed.options.shell; |
| } else if (process.platform === 'android') { |
| parsed.command = '/system/bin/sh'; |
| } else { |
| parsed.command = '/bin/sh'; |
| } |
| |
| parsed.args = ['-c', shellCommand]; |
| } |
| |
| return parsed; |
| } |
| |
| function parse(command, args, options) { |
| // Normalize arguments, similar to nodejs |
| if (args && !Array.isArray(args)) { |
| options = args; |
| args = null; |
| } |
| |
| args = args ? args.slice(0) : []; // Clone array to avoid changing the original |
| options = Object.assign({}, options); // Clone object to avoid changing the original |
| |
| // Build our parsed object |
| const parsed = { |
| command, |
| args, |
| options, |
| file: undefined, |
| original: { |
| command, |
| args, |
| }, |
| }; |
| |
| // Delegate further parsing to shell or non-shell |
| return options.shell ? parseShell(parsed) : parseNonShell(parsed); |
| } |
| |
| module.exports = parse; |