You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
125 lines
4.3 KiB
125 lines
4.3 KiB
'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;
|
|
|