parse.js 4.3 KB

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