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;
|
---|