argparse-ts
Version:
Modern CLI arguments parser for node.js
587 lines • 21.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { buildArgExtraConfig, parseArgsArray } from "./utils/utils";
import { convertToTable, formatArgHelp, tabTableRows } from "./utils/help";
import { checkAllOptionsRecognized, checkAllPositionalValuesUsed, checkEnoughPositionalValues, createValueValidator, validateArgConfig, } from "./utils/validation";
import { createValueCaster } from "./utils/cast";
import { helpAction, versionAction } from "./utils/actions";
import { ArgsParserError, StopException, isExceptionInstanceOf, } from "./exceptions";
/**
* A collection of parsed arguments.
*
* @category Classes
*/
export class ParsedArgumentsCollection {
constructor() {
/**
* A collection of parsed positional arguments.
*/
this.positionalArgs = {};
/**
* A collection of parsed optional arguments.
*/
this.optionArgs = {};
}
/**
* Returns positional arguments as a record.
*/
get positional() {
return Object.assign({}, this.positionalArgs);
}
/**
* Returns options as a record.
*/
get options() {
return Object.assign({}, this.optionArgs);
}
/**
* Retrieves an argument value.
* @param name - The name of the argument.
* @returns The value of the argument, or undefined if not found.
*/
get(name) {
var _a, _b;
const formattedName = this.formatName(name);
if (this.isPositional(name)) {
return ((_a = this.positional[formattedName]) !== null && _a !== void 0 ? _a : undefined);
}
return ((_b = this.optionArgs[formattedName]) !== null && _b !== void 0 ? _b : undefined);
}
/**
* Checks if an argument exists in the collection.
* @param name - The name of the argument.
* @returns True if the argument exists, otherwise false.
*/
has(name) {
var _a, _b;
const formattedName = this.formatName(name);
if (this.isPositional(name)) {
return ((_a = this.positional[formattedName]) !== null && _a !== void 0 ? _a : undefined) !== undefined;
}
return ((_b = this.optionArgs[formattedName]) !== null && _b !== void 0 ? _b : undefined) !== undefined;
}
/**
* Adds a new argument to the collection.
* @param name - The name of the argument.
* @param value - The value of the argument.
* @returns The instance of ParsedArgumentsCollection for chaining.
*/
add(name, value) {
const formattedName = this.formatName(name);
if (this.isPositional(name)) {
this.positionalArgs[formattedName] = value;
}
else {
this.optionArgs[formattedName] = value;
}
return this;
}
/**
* Formats the argument name by removing leading dashes.
* @param name - The argument name.
* @returns The formatted name.
*/
formatName(name) {
return name.replace(/^--/, '');
}
/**
* Checks if the argument is positional.
* @param name - The argument name.
* @returns True if the argument is positional, otherwise false.
*/
isPositional(name) {
return !name.startsWith('--');
}
}
/**
* Parser for command-line arguments.
*
* @category Classes
*/
export class ArgsParser {
/**
* Constructs an instance of ArgsParser with the provided argument configurations.
*
* @param config - Configuration options for the parser.
* @param args - An array of argument configurations.
*/
constructor(config, args = []) {
/**
* Maps argument names to their configuration.
*/
this.argsMap = new Map();
/**
* Maps argument aliases to their corresponding names.
*/
this.aliasMap = new Map();
/**
* Tracks used argument names and aliases to prevent duplicates.
*/
this.usedArgs = new Set();
this.config = this.formatConfig(config);
for (const arg of args) {
this.addArgument(arg);
}
}
/**
* Retrieves the help message.
*/
get help() {
const positionalArgs = this.getPositionalArguments();
const optionArgs = this.getOptionArguments();
const positionalArgsRows = [];
for (const arg of positionalArgs) {
positionalArgsRows.push(...formatArgHelp(arg));
}
const optionalArgsRows = [];
for (const arg of optionArgs) {
optionalArgsRows.push(...formatArgHelp(arg));
}
const result = [];
if (positionalArgsRows.length > 0) {
result.push(['Positional arguments:'], [''], ...tabTableRows(positionalArgsRows));
}
if (optionalArgsRows.length > 0) {
result.push(['Options:'], [''], ...tabTableRows(optionalArgsRows));
}
return convertToTable(result, 1);
}
/**
* Adds a new argument configuration to the parser.
*
* @param config - The argument configuration.
* @returns The instance of ArgsParser for chaining.
*
* @example
* ```
* const parser = new ArgsParser();
* parser.addArgument({
* name: '--flag',
* alias: '-f',
* type: 'boolean',
* const: true,
* default: false,
* });
* ```
*/
addArgument(config) {
validateArgConfig(config, this.getUsedArgs());
this.argsMap.set(config.name, this.extendArgConfig(config));
this.usedArgs.add(config.name);
if (config.alias !== undefined) {
this.aliasMap.set(config.alias, config.name);
this.usedArgs.add(config.alias);
}
return this;
}
/**
* Adds an action configuration to the parser.
*
* @param config - The action configuration.
*
* @returns The updated parser.
*/
addAction(config) {
return this.addArgument(Object.assign(Object.assign({}, config), { type: 'boolean', const: true }));
}
/**
* Adds a help action to the parser.
*
* @param name - The name of the help action.
* @param alias - The alias of the help action.
*
* @returns The updated parser.
*/
addHelpAction(name, alias) {
return this.addAction({
name: name !== null && name !== void 0 ? name : '--help',
alias: (name === undefined && alias === undefined) ? '-h' : alias,
action: 'help',
description: 'Show help and exit',
});
}
/**
* Adds a version action to the parser.
*
* @param name - The name of the version action.
* @param alias - The alias of the version action.
*
* @returns The updated parser.
*/
addVersionAction(name, alias) {
return this.addAction({
name: name !== null && name !== void 0 ? name : '--version',
alias: (name === undefined && alias === undefined) ? '-v' : alias,
action: 'version',
description: 'Show version and exit',
});
}
/**
* Parses the given argument string and returns a collection of parsed arguments.
*
* @param argv - The argument string.
*
* @returns A ParsedArgumentsCollection containing the parsed arguments.
*
* @example
* ```
* const parser = new ArgsParser([
* {
* name: 'action',
* description: 'Action to perform',
* type: 'string',
* },
* {
* name: '--flag',
* description: 'Flag',
* alias: '-f',
* type: 'boolean',
* const: true,
* default: false,
* },
* {
* name: '--values',
* description: 'Values',
* alias: '-v',
* type: 'string',
* nargs: '*',
* default: [],
* }
* ]);
*
* const argv = ['make', '--flag', '-v', 'a', 'b', 'c'];
* const parsedArgs = parser.parse(argv);
*
* console.log(parsedArgs.positional); // { action: 'make' }
* console.log(parsedArgs.optional); // { flag: true, values: ['a', 'b', 'c'] }
* console.log(parsedArgs.get('--values')); // ['a', 'b', 'c']
* ```
*/
parse(argv) {
// Initialize a new collection to store the parsed arguments with their values
const result = new ParsedArgumentsCollection();
// Parse the input arguments array into positional and optional parts
let [parsedPositional, parsedOptions] = parseArgsArray(argv);
let error = undefined;
try {
this.processPositionalArgs(parsedPositional, this.getPositionalArguments(), result);
}
catch (e) {
error = this.processException(e, error);
}
try {
this.processOptions(parsedOptions, this.getOptionArguments(), result);
}
catch (e) {
error = this.processException(e, error);
}
if (error) {
this.processExit(1, error);
}
return result;
}
/**
* Processes positional arguments.
*
* @param parsedValues - A parsed array of positional argument values.
* @param argConfigs - An array of extended argument configurations.
* @param result - The collection where parsed arguments will be added.
*
* @returns The number of positional arguments processed.
*/
processPositionalArgs(parsedValues, argConfigs, result) {
let count = 0;
// Reverse the parsed positional arguments for easier processing
parsedValues = [...parsedValues].reverse();
// Process positional arguments
for (let i = 0; i < argConfigs.length; ++i) {
const argConfig = argConfigs[i];
// Get the remaining positional arguments configurations
const remainingArgConfigs = argConfigs.slice(i + 1);
// Check if there are enough positional arguments left
checkEnoughPositionalValues(parsedValues, argConfig, remainingArgConfigs);
// Read the correct number of positional arguments
const value = this.readPositionalArgValues(parsedValues, argConfig, remainingArgConfigs);
// Add the parsed argument to the result
result.add(argConfig.name, this.processArgValue(value, argConfig, result, value.length > 0));
++count;
}
if (!this.config.ignoreUnrecognized) {
// Check if all positional arguments were used
checkAllPositionalValuesUsed(parsedValues);
}
return count;
}
/**
* Processes optional arguments.
*
* @param parsedValuesMap - A map of parsed optional argument keys to their string values.
* @param argConfigs - An array of extended argument configurations.
* @param result - The collection where parsed arguments will be added.
*
* @returns The number of optional arguments processed.
*/
processOptions(parsedValuesMap, argConfigs, result) {
let count = 0;
const argConfigsSet = new Set(argConfigs);
// Retrieve the optional argument configurations map
const argConfigsMap = this.getArgConfigMap(Object.keys(parsedValuesMap));
if (!this.config.ignoreUnrecognized) {
// Check if all options were recognized
checkAllOptionsRecognized(parsedValuesMap, argConfigsMap);
}
// Process options
for (const [key, value] of Object.entries(parsedValuesMap)) {
if (argConfigsMap[key] === undefined) {
continue;
}
const argConfig = argConfigsMap[key];
result.add(argConfig.name, this.processArgValue(value, argConfig, result, true));
argConfigsSet.delete(argConfig);
++count;
}
// Process optional arguments that were not set and add default if necessary
for (const argConfig of argConfigsSet) {
const value = this.processArgValue([], argConfig, result, false);
if ('default' in argConfig) {
result.add(argConfig.name, value);
}
++count;
}
return count;
}
processException(e, previous) {
if (isExceptionInstanceOf(e, StopException)) {
this.processExit(0, e);
}
if (previous) {
return previous;
}
if (isExceptionInstanceOf(e, ArgsParserError)) {
return e;
}
throw e;
}
processExit(exitCode, e) {
if (process !== undefined) {
if (exitCode === 0 && this.config.exitOnStop) {
process.exit(exitCode);
}
if (exitCode !== 0 && this.config.exitOnError) {
console.error(`Error: ${e.message}`);
process.exit(exitCode);
}
}
throw e;
}
/**
* Processes a single argument value.
*
* @param value - The value(s) being processed.
* @param argConfig - The configuration of the argument.
* @param parsed - The parsed result
* @param isset - Whether the argument is set.
*
* @returns The processed value, or undefined if the argument is not set.
*/
processArgValue(value, argConfig, parsed, isset) {
const validator = createValueValidator(argConfig);
const caster = createValueCaster(argConfig);
validator.validateBeforeCast(value, isset);
let result = caster.cast(value, isset);
validator.validateAfterCast(result);
if (isset && argConfig.action !== undefined) {
result = this.processAction(argConfig, parsed, result);
}
return result;
}
processAction(config, parsed, value) {
switch (true) {
case config.action === 'help':
return helpAction(value, parsed, this);
case config.action === 'version':
return versionAction(value, parsed, this);
case (typeof config.action === 'function'):
return config.action(value, parsed, this);
}
return value; // TODO maybe throw unknown action
}
/**
* Retrieves the positional arguments.
*
* @returns An array of positional argument configurations.
*/
getPositionalArguments() {
return [...this.argsMap.values()].filter((x) => x.positional);
}
/**
* Retrieves the optional arguments.
*
* @returns An array of optional argument configurations.
*/
getOptionArguments() {
return [...this.argsMap.values()].filter((x) => !x.positional);
}
/**
* Retrieves a set of used argument names and aliases.
*
* @returns A set of used argument names and aliases.
*/
getUsedArgs() {
return new Set(this.usedArgs);
}
/**
* Retrieves a map of argument configurations for the given keys.
*
* @param keys - The keys to retrieve.
*
* @returns A map of argument configurations, where each key is one of the given keys and the value
* is the corresponding ArgConfigExtended.
*/
getArgConfigMap(keys) {
const result = {};
for (const key of keys) {
result[key] = this.getArgConfig(key);
}
return result;
}
/**
* Retrieves the argument configuration for a given key.
*
* @param key - The argument key.
*
* @returns The corresponding ArgConfigExtended.
*/
getArgConfig(key) {
var _a;
return this.argsMap.get((_a = this.aliasMap.get(key)) !== null && _a !== void 0 ? _a : key);
}
/**
* Extends an argument configuration with extra information.
*
* @param config - The argument configuration.
*
* @returns The argument configuration extended with extra information.
*/
extendArgConfig(config) {
return Object.assign(Object.assign({}, config), buildArgExtraConfig(config));
}
/**
* Reads the value of the positional argument based on the nargs configuration.
*
* @param parsedValues - The parsed positional arguments.
* @param argConfig - The argument configuration.
* @param remainingArgConfigs - The remaining argument configurations.
*
* @returns The value of the positional argument.
*/
readPositionalArgValues(parsedValues, argConfig, remainingArgConfigs) {
const toReadCount = this.getArgsCountToRead(argConfig, remainingArgConfigs, parsedValues);
const value = [];
for (let i = 0; i < toReadCount; ++i) {
value.push(parsedValues.pop());
}
return value;
}
/**
* Determines the number of arguments to read based on the nargs configuration.
*
* @param argConfig - The configuration specifying how many arguments can be read.
* @param remainingArgConfigs - The remaining arguments to read.
* @param values - The total number of available arguments.
*
* @returns The number of arguments to read.
*/
getArgsCountToRead(argConfig, remainingArgConfigs, values) {
// If the argument is required and not multiple, read one value.
if (argConfig.required && !argConfig.multiple) {
return Math.min(1, values.length);
}
// The total number of arguments that must be read by the remaining arguments.
const minRemainingValuesCount = remainingArgConfigs.reduce((acc, x) => acc + x.minValuesCount, 0);
// The maximum number of arguments that can be read for the current argument.
let maxCurrentValuesCount = values.length - minRemainingValuesCount;
if (maxCurrentValuesCount < 0) {
// If there are not enough values to read, read as many as are available.
maxCurrentValuesCount = Math.min(argConfig.minValuesCount, values.length);
}
// If the argument is not multiple, read only one value.
if (!argConfig.multiple) {
return Math.min(1, maxCurrentValuesCount);
}
// If the argument has a fixed number of values, read that many.
if (argConfig.valuesCount !== undefined) {
return Math.min(argConfig.valuesCount, maxCurrentValuesCount);
}
// Otherwise, read as many values as are available.
return maxCurrentValuesCount;
}
formatConfig(config) {
var _a, _b, _c;
return Object.assign(Object.assign({}, config), { exitOnError: (_a = config.exitOnError) !== null && _a !== void 0 ? _a : true, exitOnStop: (_b = config.exitOnStop) !== null && _b !== void 0 ? _b : true, ignoreUnrecognized: (_c = config.ignoreUnrecognized) !== null && _c !== void 0 ? _c : false });
}
}
/**
* A class representing a router for command-line arguments.
*
* @category Classes
* @category Router
*/
export class Router {
/**
* Creates a new Router instance.
*
* @param config - The configuration for the router.
* @param routes - The routes for the router.
*/
constructor(config, routes) {
this.routes = routes;
this.parser = new ArgsParser(Object.assign(Object.assign({}, config), { ignoreUnrecognized: true }));
this.parser.addArgument({
name: 'action',
type: 'string',
description: 'Action to run',
choices: Object.keys(routes),
});
this.parser.addHelpAction();
this.parser.addVersionAction();
}
/**
* Runs the router.
*
* @param argv - The argument string.
*/
run(argv) {
const passedArgv = argv !== null && argv !== void 0 ? argv : process.argv.slice(2);
const parsed = this.parser.parse(passedArgv);
const actionName = parsed.get('action');
const actionArgsParser = new ArgsParser({
name: `${this.parser.config.name} ${actionName}`,
});
this.routes[actionName](actionArgsParser, passedArgv.slice(1));
}
/**
* Runs the router asynchronously.
*
* @param argv - The argument string.
*/
runAsync(argv) {
return __awaiter(this, void 0, void 0, function* () {
const passedArgv = argv !== null && argv !== void 0 ? argv : process.argv.slice(2);
const parsed = this.parser.parse(passedArgv);
const actionName = parsed.get('action');
const actionArgsParser = new ArgsParser({
name: `${this.parser.config.name} ${actionName}`,
});
yield this.routes[actionName](actionArgsParser, passedArgv.slice(1));
});
}
}
//# sourceMappingURL=classes.js.map