index.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. 'use strict';
  2. const flatten = require('flat');
  3. const camelcase = require('camelcase');
  4. const decamelize = require('decamelize');
  5. const isPlainObj = require('is-plain-obj');
  6. function isAlias(key, alias) {
  7. // TODO Switch to Object.values one Node.js 6 is dropped
  8. return Object.keys(alias).some((id) => [].concat(alias[id]).indexOf(key) !== -1);
  9. }
  10. function hasDefaultValue(key, value, defaults) {
  11. return value === defaults[key];
  12. }
  13. function isCamelCased(key, argv) {
  14. return /[A-Z]/.test(key) && camelcase(key) === key && // Is it camel case?
  15. argv[decamelize(key, '-')] != null; // Is the standard version defined?
  16. }
  17. function keyToFlag(key) {
  18. return key.length === 1 ? `-${key}` : `--${key}`;
  19. }
  20. function parseCommand(cmd) {
  21. const extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ');
  22. const splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/);
  23. const bregex = /\.*[\][<>]/g;
  24. const firstCommand = splitCommand.shift();
  25. if (!firstCommand) { throw new Error(`No command found in: ${cmd}`); }
  26. const parsedCommand = {
  27. cmd: firstCommand.replace(bregex, ''),
  28. demanded: [],
  29. optional: [],
  30. };
  31. splitCommand.forEach((cmd, i) => {
  32. let variadic = false;
  33. cmd = cmd.replace(/\s/g, '');
  34. if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) { variadic = true; }
  35. if (/^\[/.test(cmd)) {
  36. parsedCommand.optional.push({
  37. cmd: cmd.replace(bregex, '').split('|'),
  38. variadic,
  39. });
  40. } else {
  41. parsedCommand.demanded.push({
  42. cmd: cmd.replace(bregex, '').split('|'),
  43. variadic,
  44. });
  45. }
  46. });
  47. return parsedCommand;
  48. }
  49. function unparseOption(key, value, unparsed) {
  50. if (typeof value === 'string') {
  51. unparsed.push(keyToFlag(key), value);
  52. } else if (value === true) {
  53. unparsed.push(keyToFlag(key));
  54. } else if (value === false) {
  55. unparsed.push(`--no-${key}`);
  56. } else if (Array.isArray(value)) {
  57. value.forEach((item) => unparseOption(key, item, unparsed));
  58. } else if (isPlainObj(value)) {
  59. const flattened = flatten(value, { safe: true });
  60. for (const flattenedKey in flattened) {
  61. if (!isCamelCased(flattenedKey, flattened)) {
  62. unparseOption(`${key}.${flattenedKey}`, flattened[flattenedKey], unparsed);
  63. }
  64. }
  65. // Fallback case (numbers and other types)
  66. } else if (value != null) {
  67. unparsed.push(keyToFlag(key), `${value}`);
  68. }
  69. }
  70. function unparsePositional(argv, options, unparsed) {
  71. const knownPositional = [];
  72. // Unparse command if set, collecting all known positional arguments
  73. // e.g.: build <first> <second> <rest...>
  74. if (options.command) {
  75. const { 0: cmd, index } = options.command.match(/[^<[]*/);
  76. const { demanded, optional } = parseCommand(`foo ${options.command.substr(index + cmd.length)}`);
  77. // Push command (can be a deep command)
  78. unparsed.push(...cmd.trim().split(/\s+/));
  79. // Push positional arguments
  80. [...demanded, ...optional].forEach(({ cmd: cmds, variadic }) => {
  81. knownPositional.push(...cmds);
  82. const cmd = cmds[0];
  83. const args = (variadic ? argv[cmd] || [] : [argv[cmd]])
  84. .filter((arg) => arg != null)
  85. .map((arg) => `${arg}`);
  86. unparsed.push(...args);
  87. });
  88. }
  89. // Unparse unkown positional arguments
  90. argv._ && unparsed.push(...argv._.slice(knownPositional.length));
  91. return knownPositional;
  92. }
  93. function unparseOptions(argv, options, knownPositional, unparsed) {
  94. for (const key of Object.keys(argv)) {
  95. const value = argv[key];
  96. if (
  97. // Remove positional arguments
  98. knownPositional.includes(key) ||
  99. // Remove special _, -- and $0
  100. ['_', '--', '$0'].includes(key) ||
  101. // Remove aliases
  102. isAlias(key, options.alias) ||
  103. // Remove default values
  104. hasDefaultValue(key, value, options.default) ||
  105. // Remove camel-cased
  106. isCamelCased(key, argv)
  107. ) {
  108. continue;
  109. }
  110. unparseOption(key, argv[key], unparsed);
  111. }
  112. }
  113. function unparseEndOfOptions(argv, options, unparsed) {
  114. // Unparse ending (--) arguments if set
  115. argv['--'] && unparsed.push('--', ...argv['--']);
  116. }
  117. // ------------------------------------------------------------
  118. function unparser(argv, options) {
  119. options = Object.assign({
  120. alias: {},
  121. default: {},
  122. command: null,
  123. }, options);
  124. const unparsed = [];
  125. // Unparse known & unknown positional arguments (foo <first> <second> [rest...])
  126. // All known positional will be returned so that they are not added as flags
  127. const knownPositional = unparsePositional(argv, options, unparsed);
  128. // Unparse option arguments (--foo hello --bar hi)
  129. unparseOptions(argv, options, knownPositional, unparsed);
  130. // Unparse "end-of-options" arguments (stuff after " -- ")
  131. unparseEndOfOptions(argv, options, unparsed);
  132. return unparsed;
  133. }
  134. module.exports = unparser;