time-to-botec

Benchmark sampling in different programming languages
Log | Files | Refs | README

option.js (8631B)


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