You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

91 lines
3.0 KiB

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