You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
4.8 KiB
209 lines
4.8 KiB
4 weeks ago
|
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;
|