|
- const { InvalidArgumentError } = require('./error.js');
-
- // @ts-check
-
- class Option {
- /**
- * Initialize a new `Option` with the given `flags` and `description`.
- *
- * @param {string} flags
- * @param {string} [description]
- */
-
- constructor(flags, description) {
- this.flags = flags;
- this.description = description || '';
-
- this.required = flags.includes('<'); // A value must be supplied when the option is specified.
- this.optional = flags.includes('['); // A value is optional when the option is specified.
- // variadic test ignores <value,...> et al which might be used to describe custom splitting of single argument
- this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
- this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
- const optionFlags = splitOptionFlags(flags);
- this.short = optionFlags.shortFlag;
- this.long = optionFlags.longFlag;
- this.negate = false;
- if (this.long) {
- this.negate = this.long.startsWith('--no-');
- }
- this.defaultValue = undefined;
- this.defaultValueDescription = undefined;
- this.presetArg = undefined;
- this.envVar = undefined;
- this.parseArg = undefined;
- this.hidden = false;
- this.argChoices = undefined;
- this.conflictsWith = [];
- this.implied = undefined;
- }
-
- /**
- * Set the default value, and optionally supply the description to be displayed in the help.
- *
- * @param {any} value
- * @param {string} [description]
- * @return {Option}
- */
-
- default(value, description) {
- this.defaultValue = value;
- this.defaultValueDescription = description;
- return this;
- }
-
- /**
- * Preset to use when option used without option-argument, especially optional but also boolean and negated.
- * The custom processing (parseArg) is called.
- *
- * @example
- * new Option('--color').default('GREYSCALE').preset('RGB');
- * new Option('--donate [amount]').preset('20').argParser(parseFloat);
- *
- * @param {any} arg
- * @return {Option}
- */
-
- preset(arg) {
- this.presetArg = arg;
- return this;
- }
-
- /**
- * Add option name(s) that conflict with this option.
- * An error will be displayed if conflicting options are found during parsing.
- *
- * @example
- * new Option('--rgb').conflicts('cmyk');
- * new Option('--js').conflicts(['ts', 'jsx']);
- *
- * @param {string | string[]} names
- * @return {Option}
- */
-
- conflicts(names) {
- this.conflictsWith = this.conflictsWith.concat(names);
- return this;
- }
-
- /**
- * Specify implied option values for when this option is set and the implied options are not.
- *
- * The custom processing (parseArg) is not called on the implied values.
- *
- * @example
- * program
- * .addOption(new Option('--log', 'write logging information to file'))
- * .addOption(new Option('--trace', 'log extra details').implies({ log: 'trace.txt' }));
- *
- * @param {Object} impliedOptionValues
- * @return {Option}
- */
- implies(impliedOptionValues) {
- this.implied = Object.assign(this.implied || {}, impliedOptionValues);
- return this;
- }
-
- /**
- * Set environment variable to check for option value.
- *
- * An environment variable is only used if when processed the current option value is
- * undefined, or the source of the current value is 'default' or 'config' or 'env'.
- *
- * @param {string} name
- * @return {Option}
- */
-
- env(name) {
- this.envVar = name;
- return this;
- }
-
- /**
- * Set the custom handler for processing CLI option arguments into option values.
- *
- * @param {Function} [fn]
- * @return {Option}
- */
-
- argParser(fn) {
- this.parseArg = fn;
- return this;
- }
-
- /**
- * Whether the option is mandatory and must have a value after parsing.
- *
- * @param {boolean} [mandatory=true]
- * @return {Option}
- */
-
- makeOptionMandatory(mandatory = true) {
- this.mandatory = !!mandatory;
- return this;
- }
-
- /**
- * Hide option in help.
- *
- * @param {boolean} [hide=true]
- * @return {Option}
- */
-
- hideHelp(hide = true) {
- this.hidden = !!hide;
- return this;
- }
-
- /**
- * @api private
- */
-
- _concatValue(value, previous) {
- if (previous === this.defaultValue || !Array.isArray(previous)) {
- return [value];
- }
-
- return previous.concat(value);
- }
-
- /**
- * Only allow option value to be one of choices.
- *
- * @param {string[]} values
- * @return {Option}
- */
-
- choices(values) {
- this.argChoices = values.slice();
- this.parseArg = (arg, previous) => {
- if (!this.argChoices.includes(arg)) {
- throw new InvalidArgumentError(`Allowed choices are ${this.argChoices.join(', ')}.`);
- }
- if (this.variadic) {
- return this._concatValue(arg, previous);
- }
- return arg;
- };
- return this;
- }
-
- /**
- * Return option name.
- *
- * @return {string}
- */
-
- name() {
- if (this.long) {
- return this.long.replace(/^--/, '');
- }
- return this.short.replace(/^-/, '');
- }
-
- /**
- * Return option name, in a camelcase format that can be used
- * as a object attribute key.
- *
- * @return {string}
- * @api private
- */
-
- attributeName() {
- return camelcase(this.name().replace(/^no-/, ''));
- }
-
- /**
- * Check if `arg` matches the short or long flag.
- *
- * @param {string} arg
- * @return {boolean}
- * @api private
- */
-
- is(arg) {
- return this.short === arg || this.long === arg;
- }
-
- /**
- * Return whether a boolean option.
- *
- * Options are one of boolean, negated, required argument, or optional argument.
- *
- * @return {boolean}
- * @api private
- */
-
- isBoolean() {
- return !this.required && !this.optional && !this.negate;
- }
- }
-
- /**
- * This class is to make it easier to work with dual options, without changing the existing
- * implementation. We support separate dual options for separate positive and negative options,
- * like `--build` and `--no-build`, which share a single option value. This works nicely for some
- * use cases, but is tricky for others where we want separate behaviours despite
- * the single shared option value.
- */
- class DualOptions {
- /**
- * @param {Option[]} options
- */
- constructor(options) {
- this.positiveOptions = new Map();
- this.negativeOptions = new Map();
- this.dualOptions = new Set();
- options.forEach(option => {
- if (option.negate) {
- this.negativeOptions.set(option.attributeName(), option);
- } else {
- this.positiveOptions.set(option.attributeName(), option);
- }
- });
- this.negativeOptions.forEach((value, key) => {
- if (this.positiveOptions.has(key)) {
- this.dualOptions.add(key);
- }
- });
- }
-
- /**
- * Did the value come from the option, and not from possible matching dual option?
- *
- * @param {any} value
- * @param {Option} option
- * @returns {boolean}
- */
- valueFromOption(value, option) {
- const optionKey = option.attributeName();
- if (!this.dualOptions.has(optionKey)) return true;
-
- // Use the value to deduce if (probably) came from the option.
- const preset = this.negativeOptions.get(optionKey).presetArg;
- const negativeValue = (preset !== undefined) ? preset : false;
- return option.negate === (negativeValue === value);
- }
- }
-
- /**
- * Convert string from kebab-case to camelCase.
- *
- * @param {string} str
- * @return {string}
- * @api private
- */
-
- function camelcase(str) {
- return str.split('-').reduce((str, word) => {
- return str + word[0].toUpperCase() + word.slice(1);
- });
- }
-
- /**
- * Split the short and long flag out of something like '-m,--mixed <value>'
- *
- * @api private
- */
-
- function splitOptionFlags(flags) {
- let shortFlag;
- let longFlag;
- // Use original very loose parsing to maintain backwards compatibility for now,
- // which allowed for example unintended `-sw, --short-word` [sic].
- const flagParts = flags.split(/[ |,]+/);
- if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1])) shortFlag = flagParts.shift();
- longFlag = flagParts.shift();
- // Add support for lone short flag without significantly changing parsing!
- if (!shortFlag && /^-[^-]$/.test(longFlag)) {
- shortFlag = longFlag;
- longFlag = undefined;
- }
- return { shortFlag, longFlag };
- }
-
- exports.Option = Option;
- exports.splitOptionFlags = splitOptionFlags;
- exports.DualOptions = DualOptions;
|