209 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			4.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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.envVar = undefined;
 | 
						|
    this.parseArg = undefined;
 | 
						|
    this.hidden = false;
 | 
						|
    this.argChoices = 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;
 | 
						|
  };
 | 
						|
 | 
						|
  /**
 | 
						|
   * Set environment variable to check for option value.
 | 
						|
   * Priority order of option values is default < env < cli
 | 
						|
   *
 | 
						|
   * @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;
 | 
						|
    this.parseArg = (arg, previous) => {
 | 
						|
      if (!values.includes(arg)) {
 | 
						|
        throw new InvalidArgumentError(`Allowed choices are ${values.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;
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * 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;
 |