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');
10 const isWin = process.platform === 'win32';
11 const isExecutableRegExp = /\.(?:com|exe)$/i;
12 const isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
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;
17 function detectShebang(parsed) {
18 parsed.file = resolveCommand(parsed);
20 const shebang = parsed.file && readShebang(parsed.file);
23 parsed.args.unshift(parsed.file);
24 parsed.command = shebang;
26 return resolveCommand(parsed);
32 function parseNonShell(parsed) {
37 // Detect & add support for shebangs
38 const commandFile = detectShebang(parsed);
40 // We don't need a shell if the command filename is an executable
41 const needsShell = !isExecutableRegExp.test(commandFile);
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);
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);
56 // Escape command & arguments
57 parsed.command = escape.command(parsed.command);
58 parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
60 const shellCommand = [parsed.command].concat(parsed.args).join(' ');
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
70 function parseShell(parsed) {
71 // If node supports the shell option, there's no need to mimic its behavior
72 if (supportsShellOption) {
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(' ');
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
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';
90 parsed.command = '/bin/sh';
93 parsed.args = ['-c', shellCommand];
99 function parse(command, args, options) {
100 // Normalize arguments, similar to nodejs
101 if (args && !Array.isArray(args)) {
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
109 // Build our parsed object
121 // Delegate further parsing to shell or non-shell
122 return options.shell ? parseShell(parsed) : parseNonShell(parsed);
125 module.exports = parse;