1'use strict'; 2 3var resolveCommand = require('./util/resolveCommand'); 4var hasEmptyArgumentBug = require('./util/hasEmptyArgumentBug'); 5var escapeArgument = require('./util/escapeArgument'); 6var escapeCommand = require('./util/escapeCommand'); 7var readShebang = require('./util/readShebang'); 8 9var isWin = process.platform === 'win32'; 10var skipShellRegExp = /\.(?:com|exe)$/i; 11 12// Supported in Node >= 6 and >= 4.8 13var supportsShellOption = parseInt(process.version.substr(1).split('.')[0], 10) >= 6 || 14 parseInt(process.version.substr(1).split('.')[0], 10) === 4 && parseInt(process.version.substr(1).split('.')[1], 10) >= 8; 15 16function parseNonShell(parsed) { 17 var shebang; 18 var needsShell; 19 var applyQuotes; 20 21 if (!isWin) { 22 return parsed; 23 } 24 25 // Detect & add support for shebangs 26 parsed.file = resolveCommand(parsed.command); 27 parsed.file = parsed.file || resolveCommand(parsed.command, true); 28 shebang = parsed.file && readShebang(parsed.file); 29 30 if (shebang) { 31 parsed.args.unshift(parsed.file); 32 parsed.command = shebang; 33 needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(resolveCommand(shebang) || resolveCommand(shebang, true)); 34 } else { 35 needsShell = hasEmptyArgumentBug || !skipShellRegExp.test(parsed.file); 36 } 37 38 // If a shell is required, use cmd.exe and take care of escaping everything correctly 39 if (needsShell) { 40 // Escape command & arguments 41 applyQuotes = (parsed.command !== 'echo'); // Do not quote arguments for the special "echo" command 42 parsed.command = escapeCommand(parsed.command); 43 parsed.args = parsed.args.map(function (arg) { 44 return escapeArgument(arg, applyQuotes); 45 }); 46 47 // Make use of cmd.exe 48 parsed.args = ['/d', '/s', '/c', '"' + parsed.command + (parsed.args.length ? ' ' + parsed.args.join(' ') : '') + '"']; 49 parsed.command = process.env.comspec || 'cmd.exe'; 50 parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped 51 } 52 53 return parsed; 54} 55 56function parseShell(parsed) { 57 var shellCommand; 58 59 // If node supports the shell option, there's no need to mimic its behavior 60 if (supportsShellOption) { 61 return parsed; 62 } 63 64 // Mimic node shell option, see: https://github.com/nodejs/node/blob/b9f6a2dc059a1062776133f3d4fd848c4da7d150/lib/child_process.js#L335 65 shellCommand = [parsed.command].concat(parsed.args).join(' '); 66 67 if (isWin) { 68 parsed.command = typeof parsed.options.shell === 'string' ? parsed.options.shell : process.env.comspec || 'cmd.exe'; 69 parsed.args = ['/d', '/s', '/c', '"' + shellCommand + '"']; 70 parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped 71 } else { 72 if (typeof parsed.options.shell === 'string') { 73 parsed.command = parsed.options.shell; 74 } else if (process.platform === 'android') { 75 parsed.command = '/system/bin/sh'; 76 } else { 77 parsed.command = '/bin/sh'; 78 } 79 80 parsed.args = ['-c', shellCommand]; 81 } 82 83 return parsed; 84} 85 86// ------------------------------------------------ 87 88function parse(command, args, options) { 89 var parsed; 90 91 // Normalize arguments, similar to nodejs 92 if (args && !Array.isArray(args)) { 93 options = args; 94 args = null; 95 } 96 97 args = args ? args.slice(0) : []; // Clone array to avoid changing the original 98 options = options || {}; 99 100 // Build our parsed object 101 parsed = { 102 command: command, 103 args: args, 104 options: options, 105 file: undefined, 106 original: command, 107 }; 108 109 // Delegate further parsing to shell or non-shell 110 return options.shell ? parseShell(parsed) : parseNonShell(parsed); 111} 112 113module.exports = parse; 114