option.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. const { InvalidArgumentError } = require('./error.js');
  2. // @ts-check
  3. class Option {
  4. /**
  5. * Initialize a new `Option` with the given `flags` and `description`.
  6. *
  7. * @param {string} flags
  8. * @param {string} [description]
  9. */
  10. constructor(flags, description) {
  11. this.flags = flags;
  12. this.description = description || '';
  13. this.required = flags.includes('<'); // A value must be supplied when the option is specified.
  14. this.optional = flags.includes('['); // A value is optional when the option is specified.
  15. // variadic test ignores <value,...> et al which might be used to describe custom splitting of single argument
  16. this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
  17. this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
  18. const optionFlags = splitOptionFlags(flags);
  19. this.short = optionFlags.shortFlag;
  20. this.long = optionFlags.longFlag;
  21. this.negate = false;
  22. if (this.long) {
  23. this.negate = this.long.startsWith('--no-');
  24. }
  25. this.defaultValue = undefined;
  26. this.defaultValueDescription = undefined;
  27. this.envVar = undefined;
  28. this.parseArg = undefined;
  29. this.hidden = false;
  30. this.argChoices = undefined;
  31. }
  32. /**
  33. * Set the default value, and optionally supply the description to be displayed in the help.
  34. *
  35. * @param {any} value
  36. * @param {string} [description]
  37. * @return {Option}
  38. */
  39. default(value, description) {
  40. this.defaultValue = value;
  41. this.defaultValueDescription = description;
  42. return this;
  43. };
  44. /**
  45. * Set environment variable to check for option value.
  46. * Priority order of option values is default < env < cli
  47. *
  48. * @param {string} name
  49. * @return {Option}
  50. */
  51. env(name) {
  52. this.envVar = name;
  53. return this;
  54. };
  55. /**
  56. * Set the custom handler for processing CLI option arguments into option values.
  57. *
  58. * @param {Function} [fn]
  59. * @return {Option}
  60. */
  61. argParser(fn) {
  62. this.parseArg = fn;
  63. return this;
  64. };
  65. /**
  66. * Whether the option is mandatory and must have a value after parsing.
  67. *
  68. * @param {boolean} [mandatory=true]
  69. * @return {Option}
  70. */
  71. makeOptionMandatory(mandatory = true) {
  72. this.mandatory = !!mandatory;
  73. return this;
  74. };
  75. /**
  76. * Hide option in help.
  77. *
  78. * @param {boolean} [hide=true]
  79. * @return {Option}
  80. */
  81. hideHelp(hide = true) {
  82. this.hidden = !!hide;
  83. return this;
  84. };
  85. /**
  86. * @api private
  87. */
  88. _concatValue(value, previous) {
  89. if (previous === this.defaultValue || !Array.isArray(previous)) {
  90. return [value];
  91. }
  92. return previous.concat(value);
  93. }
  94. /**
  95. * Only allow option value to be one of choices.
  96. *
  97. * @param {string[]} values
  98. * @return {Option}
  99. */
  100. choices(values) {
  101. this.argChoices = values;
  102. this.parseArg = (arg, previous) => {
  103. if (!values.includes(arg)) {
  104. throw new InvalidArgumentError(`Allowed choices are ${values.join(', ')}.`);
  105. }
  106. if (this.variadic) {
  107. return this._concatValue(arg, previous);
  108. }
  109. return arg;
  110. };
  111. return this;
  112. };
  113. /**
  114. * Return option name.
  115. *
  116. * @return {string}
  117. */
  118. name() {
  119. if (this.long) {
  120. return this.long.replace(/^--/, '');
  121. }
  122. return this.short.replace(/^-/, '');
  123. };
  124. /**
  125. * Return option name, in a camelcase format that can be used
  126. * as a object attribute key.
  127. *
  128. * @return {string}
  129. * @api private
  130. */
  131. attributeName() {
  132. return camelcase(this.name().replace(/^no-/, ''));
  133. };
  134. /**
  135. * Check if `arg` matches the short or long flag.
  136. *
  137. * @param {string} arg
  138. * @return {boolean}
  139. * @api private
  140. */
  141. is(arg) {
  142. return this.short === arg || this.long === arg;
  143. };
  144. }
  145. /**
  146. * Convert string from kebab-case to camelCase.
  147. *
  148. * @param {string} str
  149. * @return {string}
  150. * @api private
  151. */
  152. function camelcase(str) {
  153. return str.split('-').reduce((str, word) => {
  154. return str + word[0].toUpperCase() + word.slice(1);
  155. });
  156. }
  157. /**
  158. * Split the short and long flag out of something like '-m,--mixed <value>'
  159. *
  160. * @api private
  161. */
  162. function splitOptionFlags(flags) {
  163. let shortFlag;
  164. let longFlag;
  165. // Use original very loose parsing to maintain backwards compatibility for now,
  166. // which allowed for example unintended `-sw, --short-word` [sic].
  167. const flagParts = flags.split(/[ |,]+/);
  168. if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) shortFlag = flagParts.shift();
  169. longFlag = flagParts.shift();
  170. // Add support for lone short flag without significantly changing parsing!
  171. if (!shortFlag && /^-[^-]$/.test(longFlag)) {
  172. shortFlag = longFlag;
  173. longFlag = undefined;
  174. }
  175. return { shortFlag, longFlag };
  176. }
  177. exports.Option = Option;
  178. exports.splitOptionFlags = splitOptionFlags;