option.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. const { InvalidArgumentError } = require('./error.js');
  2. class Option {
  3. /**
  4. * Initialize a new `Option` with the given `flags` and `description`.
  5. *
  6. * @param {string} flags
  7. * @param {string} [description]
  8. */
  9. constructor(flags, description) {
  10. this.flags = flags;
  11. this.description = description || '';
  12. this.required = flags.includes('<'); // A value must be supplied when the option is specified.
  13. this.optional = flags.includes('['); // A value is optional when the option is specified.
  14. // variadic test ignores <value,...> et al which might be used to describe custom splitting of single argument
  15. this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
  16. this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
  17. const optionFlags = splitOptionFlags(flags);
  18. this.short = optionFlags.shortFlag;
  19. this.long = optionFlags.longFlag;
  20. this.negate = false;
  21. if (this.long) {
  22. this.negate = this.long.startsWith('--no-');
  23. }
  24. this.defaultValue = undefined;
  25. this.defaultValueDescription = undefined;
  26. this.presetArg = undefined;
  27. this.envVar = undefined;
  28. this.parseArg = undefined;
  29. this.hidden = false;
  30. this.argChoices = undefined;
  31. this.conflictsWith = [];
  32. this.implied = undefined;
  33. }
  34. /**
  35. * Set the default value, and optionally supply the description to be displayed in the help.
  36. *
  37. * @param {*} value
  38. * @param {string} [description]
  39. * @return {Option}
  40. */
  41. default(value, description) {
  42. this.defaultValue = value;
  43. this.defaultValueDescription = description;
  44. return this;
  45. }
  46. /**
  47. * Preset to use when option used without option-argument, especially optional but also boolean and negated.
  48. * The custom processing (parseArg) is called.
  49. *
  50. * @example
  51. * new Option('--color').default('GREYSCALE').preset('RGB');
  52. * new Option('--donate [amount]').preset('20').argParser(parseFloat);
  53. *
  54. * @param {*} arg
  55. * @return {Option}
  56. */
  57. preset(arg) {
  58. this.presetArg = arg;
  59. return this;
  60. }
  61. /**
  62. * Add option name(s) that conflict with this option.
  63. * An error will be displayed if conflicting options are found during parsing.
  64. *
  65. * @example
  66. * new Option('--rgb').conflicts('cmyk');
  67. * new Option('--js').conflicts(['ts', 'jsx']);
  68. *
  69. * @param {(string | string[])} names
  70. * @return {Option}
  71. */
  72. conflicts(names) {
  73. this.conflictsWith = this.conflictsWith.concat(names);
  74. return this;
  75. }
  76. /**
  77. * Specify implied option values for when this option is set and the implied options are not.
  78. *
  79. * The custom processing (parseArg) is not called on the implied values.
  80. *
  81. * @example
  82. * program
  83. * .addOption(new Option('--log', 'write logging information to file'))
  84. * .addOption(new Option('--trace', 'log extra details').implies({ log: 'trace.txt' }));
  85. *
  86. * @param {object} impliedOptionValues
  87. * @return {Option}
  88. */
  89. implies(impliedOptionValues) {
  90. let newImplied = impliedOptionValues;
  91. if (typeof impliedOptionValues === 'string') {
  92. // string is not documented, but easy mistake and we can do what user probably intended.
  93. newImplied = { [impliedOptionValues]: true };
  94. }
  95. this.implied = Object.assign(this.implied || {}, newImplied);
  96. return this;
  97. }
  98. /**
  99. * Set environment variable to check for option value.
  100. *
  101. * An environment variable is only used if when processed the current option value is
  102. * undefined, or the source of the current value is 'default' or 'config' or 'env'.
  103. *
  104. * @param {string} name
  105. * @return {Option}
  106. */
  107. env(name) {
  108. this.envVar = name;
  109. return this;
  110. }
  111. /**
  112. * Set the custom handler for processing CLI option arguments into option values.
  113. *
  114. * @param {Function} [fn]
  115. * @return {Option}
  116. */
  117. argParser(fn) {
  118. this.parseArg = fn;
  119. return this;
  120. }
  121. /**
  122. * Whether the option is mandatory and must have a value after parsing.
  123. *
  124. * @param {boolean} [mandatory=true]
  125. * @return {Option}
  126. */
  127. makeOptionMandatory(mandatory = true) {
  128. this.mandatory = !!mandatory;
  129. return this;
  130. }
  131. /**
  132. * Hide option in help.
  133. *
  134. * @param {boolean} [hide=true]
  135. * @return {Option}
  136. */
  137. hideHelp(hide = true) {
  138. this.hidden = !!hide;
  139. return this;
  140. }
  141. /**
  142. * @package
  143. */
  144. _concatValue(value, previous) {
  145. if (previous === this.defaultValue || !Array.isArray(previous)) {
  146. return [value];
  147. }
  148. return previous.concat(value);
  149. }
  150. /**
  151. * Only allow option value to be one of choices.
  152. *
  153. * @param {string[]} values
  154. * @return {Option}
  155. */
  156. choices(values) {
  157. this.argChoices = values.slice();
  158. this.parseArg = (arg, previous) => {
  159. if (!this.argChoices.includes(arg)) {
  160. throw new InvalidArgumentError(
  161. `Allowed choices are ${this.argChoices.join(', ')}.`,
  162. );
  163. }
  164. if (this.variadic) {
  165. return this._concatValue(arg, previous);
  166. }
  167. return arg;
  168. };
  169. return this;
  170. }
  171. /**
  172. * Return option name.
  173. *
  174. * @return {string}
  175. */
  176. name() {
  177. if (this.long) {
  178. return this.long.replace(/^--/, '');
  179. }
  180. return this.short.replace(/^-/, '');
  181. }
  182. /**
  183. * Return option name, in a camelcase format that can be used
  184. * as a object attribute key.
  185. *
  186. * @return {string}
  187. */
  188. attributeName() {
  189. return camelcase(this.name().replace(/^no-/, ''));
  190. }
  191. /**
  192. * Check if `arg` matches the short or long flag.
  193. *
  194. * @param {string} arg
  195. * @return {boolean}
  196. * @package
  197. */
  198. is(arg) {
  199. return this.short === arg || this.long === arg;
  200. }
  201. /**
  202. * Return whether a boolean option.
  203. *
  204. * Options are one of boolean, negated, required argument, or optional argument.
  205. *
  206. * @return {boolean}
  207. * @package
  208. */
  209. isBoolean() {
  210. return !this.required && !this.optional && !this.negate;
  211. }
  212. }
  213. /**
  214. * This class is to make it easier to work with dual options, without changing the existing
  215. * implementation. We support separate dual options for separate positive and negative options,
  216. * like `--build` and `--no-build`, which share a single option value. This works nicely for some
  217. * use cases, but is tricky for others where we want separate behaviours despite
  218. * the single shared option value.
  219. */
  220. class DualOptions {
  221. /**
  222. * @param {Option[]} options
  223. */
  224. constructor(options) {
  225. this.positiveOptions = new Map();
  226. this.negativeOptions = new Map();
  227. this.dualOptions = new Set();
  228. options.forEach((option) => {
  229. if (option.negate) {
  230. this.negativeOptions.set(option.attributeName(), option);
  231. } else {
  232. this.positiveOptions.set(option.attributeName(), option);
  233. }
  234. });
  235. this.negativeOptions.forEach((value, key) => {
  236. if (this.positiveOptions.has(key)) {
  237. this.dualOptions.add(key);
  238. }
  239. });
  240. }
  241. /**
  242. * Did the value come from the option, and not from possible matching dual option?
  243. *
  244. * @param {*} value
  245. * @param {Option} option
  246. * @returns {boolean}
  247. */
  248. valueFromOption(value, option) {
  249. const optionKey = option.attributeName();
  250. if (!this.dualOptions.has(optionKey)) return true;
  251. // Use the value to deduce if (probably) came from the option.
  252. const preset = this.negativeOptions.get(optionKey).presetArg;
  253. const negativeValue = preset !== undefined ? preset : false;
  254. return option.negate === (negativeValue === value);
  255. }
  256. }
  257. /**
  258. * Convert string from kebab-case to camelCase.
  259. *
  260. * @param {string} str
  261. * @return {string}
  262. * @private
  263. */
  264. function camelcase(str) {
  265. return str.split('-').reduce((str, word) => {
  266. return str + word[0].toUpperCase() + word.slice(1);
  267. });
  268. }
  269. /**
  270. * Split the short and long flag out of something like '-m,--mixed <value>'
  271. *
  272. * @private
  273. */
  274. function splitOptionFlags(flags) {
  275. let shortFlag;
  276. let longFlag;
  277. // Use original very loose parsing to maintain backwards compatibility for now,
  278. // which allowed for example unintended `-sw, --short-word` [sic].
  279. const flagParts = flags.split(/[ |,]+/);
  280. if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1]))
  281. shortFlag = flagParts.shift();
  282. longFlag = flagParts.shift();
  283. // Add support for lone short flag without significantly changing parsing!
  284. if (!shortFlag && /^-[^-]$/.test(longFlag)) {
  285. shortFlag = longFlag;
  286. longFlag = undefined;
  287. }
  288. return { shortFlag, longFlag };
  289. }
  290. exports.Option = Option;
  291. exports.DualOptions = DualOptions;