getOpt.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /*
  2. * TODO:
  3. * - Validate options object and configuration
  4. * - Add support for repeat arguments
  5. * - Support list type arguments
  6. * - Support optional arguments
  7. */
  8. /*
  9. * Intro
  10. *
  11. * Very simple implementation of an option parser. It supports long and short
  12. * options, bundled options (-avd) and options with values. It receives an
  13. * argument object describing the options, their types (not to be confused with
  14. * their value types, i.e Integer, String, etc), a default value (if any), and,
  15. * possibly, functions for value validation and convertion. It returns an object
  16. * contanining key-value pairs of the defined long options.
  17. *
  18. * Options of type 'flag' will have their values set to 'true' if the option is
  19. * found at the command-line, otherwise they will be 'null'.
  20. *
  21. * This 'getOpt' also forces the definition of long option names, this is in
  22. * attempt to make the returned option object more readable. But nothing is
  23. * stopping the user from defining single letter long option names (i.e '--l').
  24. *
  25. * It also has optiona support for "bundling" (by default is turned on) both
  26. * options and option arguments. To turn bundling off, pass an object containing
  27. * { bundling: false } as the second argument.
  28. *
  29. * This implementation tests each entry of the bundled argument for the type
  30. * 'flag'. Each matching letter is parsed as an 'flag' option until until a
  31. * 'value' type option is found, at which point the remainder of the option is
  32. * evaluated as an argument.
  33. *
  34. * Brief example
  35. *
  36. * {
  37. * 'has-value':
  38. * {
  39. * type: 'value',
  40. * short: 'h',
  41. * convert: v => parseInt(v),
  42. * validate: v => v > 1
  43. * },
  44. * verbose: {
  45. * type: 'flag',
  46. * short: 'v'
  47. * }
  48. * }
  49. *
  50. * If a command with the options of '--has-value 5 --verbose' or '-h 5 -v' was
  51. * supplied, it would generate and the options object: { 'has-value': 5,
  52. * verbose: true }.
  53. *
  54. * One could also make use of bundling and supply a the argument: '-vh5', which
  55. * produce the same object.
  56. *
  57. * Argument Object
  58. *
  59. * type
  60. *
  61. * There are two types of arguments supported: arguments with user defined
  62. * values ('type: value') and arguments that act as switches ('type: flag') and
  63. * carry no user specified value. They have two strategies for parsing: type
  64. * 'value' consumes the next immediate argument; and type 'flag' doesn't consume
  65. * anything on the argument parsing loop.
  66. *
  67. * short
  68. *
  69. * A short version of the option, meant to be used with a single dash ('-')
  70. * character. Short options can be bundled together ('-abc' or '-p3000'), but
  71. * only if they are all of type 'flag', or if the there is a single 'value' type
  72. * option on the end ('-abp3000').
  73. *
  74. * default
  75. *
  76. * Default value for option in case none is specified.
  77. *
  78. * convert
  79. *
  80. * A optional function to perform any required operations on the extracted
  81. * value. Its executed before the 'validate' function.
  82. *
  83. * validate
  84. *
  85. * An optional function that tests the value of value. Its exectued after the
  86. * 'convert' function and must return a falsy or truthy value.
  87. *
  88. * Quirks
  89. *
  90. * An option that requires an argument, but that is immediately followed by
  91. * another option, will have this option set at its argument.
  92. */
  93. "use strict";
  94. import { die } from './die.js';
  95. export function getOpt(argObj, config) {
  96. const optKeys = Object.keys(argObj);
  97. let argv = process.argv.slice(2)
  98. let opts = {};
  99. let notOpts = [];
  100. let bundle = true;
  101. bundle = config.bundle;
  102. let arg;
  103. while ((arg = argv.shift())) {
  104. // Skip non-option args
  105. if (!arg.startsWith('-')) {
  106. notOpts.push(arg);
  107. continue;
  108. }
  109. // End of options
  110. if (arg == '--') break;
  111. let argOpt;
  112. let argKey;
  113. if (arg.startsWith('--')) { // long option
  114. argOpt = arg.slice(2);
  115. argKey = optKeys.find(k => k == argOpt);
  116. if (!argKey)
  117. die(`Unrecognized option: ${arg}`);
  118. } else { // short option
  119. argOpt = arg.slice(1);
  120. const opt = argOpt[0];
  121. const key = optKeys.find(k => argObj[k].short == opt);
  122. if (!key)
  123. die(`Unrecognized option '${argOpt}'`);
  124. if (Object.keys(opts).includes(key))
  125. die(`Option '-${opt}' already specified`)
  126. argKey = key;
  127. if (argOpt.length > 1) {
  128. // If bundling, either schedule remainder to be parsed as
  129. // options or arguments dependinng or option type
  130. if (bundle) {
  131. let asOpt = '-';
  132. if (argObj[argKey].type == 'value') asOpt = '';
  133. argv.unshift(`${asOpt}${argOpt.slice(1)}`);
  134. } else
  135. die(`Unrecognized option: '${arg}'`);
  136. }
  137. }
  138. const type = argObj[argKey].type;
  139. switch (type) {
  140. case 'value':
  141. let val = argv.shift();
  142. const validateFunc = argObj[argKey].validate;
  143. const convertFunc = argObj[argKey].convert;
  144. if (convertFunc) val = convertFunc(val)
  145. if (validateFunc) {
  146. if (!validateFunc(val)) throw '';
  147. }
  148. opts[argKey] = val;
  149. break;
  150. case 'flag':
  151. opts[argKey] = true;
  152. break;
  153. default:
  154. die(`Invalid ${type} for key ${argKey}`);
  155. }
  156. }
  157. if (notOpts.length >= 1)
  158. process.argv = process.argv.slice(0, 2).concat(notOpts);
  159. // Fill empty options with their default values
  160. Object.keys(argObj).forEach(k => {
  161. if (!opts[k]) {
  162. if (argObj[k].default)
  163. opts[k] = argObj[k].default;
  164. else opts[k] = null;
  165. }
  166. });
  167. return opts;
  168. }