1'use strict'; 2 3const path = require('path'); 4const resolveCommand = require('./util/resolveCommand'); 5const escape = require('./util/escape'); 6const readShebang = require('./util/readShebang'); 7 8const isWin = process.platform === 'win32'; 9const isExecutableRegExp = /\.(?:com|exe)$/i; 10const isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; 11 12function detectShebang(parsed) { 13 parsed.file = resolveCommand(parsed); 14 15 const shebang = parsed.file && readShebang(parsed.file); 16 17 if (shebang) { 18 parsed.args.unshift(parsed.file); 19 parsed.command = shebang; 20 21 return resolveCommand(parsed); 22 } 23 24 return parsed.file; 25} 26 27function parseNonShell(parsed) { 28 if (!isWin) { 29 return parsed; 30 } 31 32 // Detect & add support for shebangs 33 const commandFile = detectShebang(parsed); 34 35 // We don't need a shell if the command filename is an executable 36 const needsShell = !isExecutableRegExp.test(commandFile); 37 38 // If a shell is required, use cmd.exe and take care of escaping everything correctly 39 // Note that `forceShell` is an hidden option used only in tests 40 if (parsed.options.forceShell || needsShell) { 41 // Need to double escape meta chars if the command is a cmd-shim located in `node_modules/.bin/` 42 // The cmd-shim simply calls execute the package bin file with NodeJS, proxying any argument 43 // Because the escape of metachars with ^ gets interpreted when the cmd.exe is first called, 44 // we need to double escape them 45 const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); 46 47 // Normalize posix paths into OS compatible paths (e.g.: foo/bar -> foo\bar) 48 // This is necessary otherwise it will always fail with ENOENT in those cases 49 parsed.command = path.normalize(parsed.command); 50 51 // Escape command & arguments 52 parsed.command = escape.command(parsed.command); 53 parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars)); 54 55 const shellCommand = [parsed.command].concat(parsed.args).join(' '); 56 57 parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; 58 parsed.command = process.env.comspec || 'cmd.exe'; 59 parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped 60 } 61 62 return parsed; 63} 64 65function parse(command, args, options) { 66 // Normalize arguments, similar to nodejs 67 if (args && !Array.isArray(args)) { 68 options = args; 69 args = null; 70 } 71 72 args = args ? args.slice(0) : []; // Clone array to avoid changing the original 73 options = Object.assign({}, options); // Clone object to avoid changing the original 74 75 // Build our parsed object 76 const parsed = { 77 command, 78 args, 79 options, 80 file: undefined, 81 original: { 82 command, 83 args, 84 }, 85 }; 86 87 // Delegate further parsing to shell or non-shell 88 return options.shell ? parsed : parseNonShell(parsed); 89} 90 91module.exports = parse; 92