utils/node_modules/@rushstack/ts-command-line/lib/providers/CommandLineParser.js
2024-02-07 01:33:07 -05:00

242 lines
11 KiB
JavaScript

"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandLineParser = void 0;
const colors_1 = __importDefault(require("colors"));
const CommandLineParameterProvider_1 = require("./CommandLineParameterProvider");
const CommandLineParserExitError_1 = require("./CommandLineParserExitError");
const TabCompletionAction_1 = require("./TabCompletionAction");
/**
* The "argparse" library is a relatively advanced command-line parser with features such
* as word-wrapping and intelligible error messages (that are lacking in other similar
* libraries such as commander, yargs, and nomnom). Unfortunately, its ruby-inspired API
* is awkward to use. The abstract base classes CommandLineParser and CommandLineAction
* provide a wrapper for "argparse" that makes defining and consuming arguments quick
* and simple, and enforces that appropriate documentation is provided for each parameter.
*
* @public
*/
class CommandLineParser extends CommandLineParameterProvider_1.CommandLineParameterProvider {
constructor(options) {
var _a, _b;
super();
this._executed = false;
this._tabCompleteActionWasAdded = false;
this._options = options;
this._actions = [];
this._actionsByName = new Map();
this._argumentParser = new CommandLineParserExitError_1.CustomArgumentParser({
addHelp: true,
prog: this._options.toolFilename,
description: this._options.toolDescription,
epilog: colors_1.default.bold((_a = this._options.toolEpilog) !== null && _a !== void 0 ? _a : `For detailed help about a specific command, use: ${this._options.toolFilename} <command> -h`)
});
(_b = this.onDefineParameters) === null || _b === void 0 ? void 0 : _b.call(this);
}
/**
* Returns the list of actions that were defined for this CommandLineParser object.
*/
get actions() {
return this._actions;
}
/**
* Defines a new action that can be used with the CommandLineParser instance.
*/
addAction(action) {
if (!this._actionsSubParser) {
this._actionsSubParser = this._argumentParser.addSubparsers({
metavar: '<command>',
dest: 'action'
});
}
action._buildParser(this._actionsSubParser);
this._actions.push(action);
this._actionsByName.set(action.actionName, action);
}
/**
* Retrieves the action with the specified name. If no matching action is found,
* an exception is thrown.
*/
getAction(actionName) {
const action = this.tryGetAction(actionName);
if (!action) {
throw new Error(`The action "${actionName}" was not defined`);
}
return action;
}
/**
* Retrieves the action with the specified name. If no matching action is found,
* undefined is returned.
*/
tryGetAction(actionName) {
return this._actionsByName.get(actionName);
}
/**
* The program entry point will call this method to begin parsing command-line arguments
* and executing the corresponding action.
*
* @remarks
* The returned promise will never reject: If an error occurs, it will be printed
* to stderr, process.exitCode will be set to 1, and the promise will resolve to false.
* This simplifies the most common usage scenario where the program entry point doesn't
* want to be involved with the command-line logic, and will discard the promise without
* a then() or catch() block.
*
* If your caller wants to trap and handle errors, use {@link CommandLineParser.executeWithoutErrorHandling}
* instead.
*
* @param args - the command-line arguments to be parsed; if omitted, then
* the process.argv will be used
*/
async execute(args) {
if (this._options.enableTabCompletionAction && !this._tabCompleteActionWasAdded) {
this.addAction(new TabCompletionAction_1.TabCompleteAction(this.actions, this.parameters));
this._tabCompleteActionWasAdded = true;
}
try {
await this.executeWithoutErrorHandling(args);
return true;
}
catch (err) {
if (err instanceof CommandLineParserExitError_1.CommandLineParserExitError) {
// executeWithoutErrorHandling() handles the successful cases,
// so here we can assume err has a nonzero exit code
if (err.message) {
// eslint-disable-next-line no-console
console.error(err.message);
}
if (!process.exitCode) {
process.exitCode = err.exitCode;
}
}
else {
let message = (err.message || 'An unknown error occurred').trim();
// If the message doesn't already start with "Error:" then add a prefix
if (!/^(error|internal error|warning)\b/i.test(message)) {
message = 'Error: ' + message;
}
// eslint-disable-next-line no-console
console.error();
// eslint-disable-next-line no-console
console.error(colors_1.default.red(message));
if (!process.exitCode) {
process.exitCode = 1;
}
}
return false;
}
}
/**
* This is similar to {@link CommandLineParser.execute}, except that execution errors
* simply cause the promise to reject. It is the caller's responsibility to trap
*/
async executeWithoutErrorHandling(args) {
var _a, _b;
try {
if (this._executed) {
// In the future we could allow the same parser to be invoked multiple times
// with different arguments. We'll do that work as soon as someone encounters
// a real world need for it.
throw new Error('execute() was already called for this parser instance');
}
this._executed = true;
this._validateDefinitions();
// Register the parameters before we print help or parse the CLI
const initialState = {
parentParameterNames: new Set()
};
this._registerDefinedParameters(initialState);
if (!args) {
// 0=node.exe, 1=script name
args = process.argv.slice(2);
}
if (this.actions.length > 0) {
if (args.length === 0) {
// Parsers that use actions should print help when 0 args are provided. Allow
// actionless parsers to continue on zero args.
this._argumentParser.printHelp();
return;
}
// Alias actions may provide a list of default params to add after the action name.
// Since we don't know which params are required and which are optional, perform a
// manual search for the action name to obtain the default params and insert them if
// any are found. We will guess that the action name is the first arg that doesn't
// start with a hyphen.
const actionNameIndex = args.findIndex((x) => !x.startsWith('-'));
if (actionNameIndex !== undefined) {
const actionName = args[actionNameIndex];
const action = this.tryGetAction(actionName);
const aliasAction = action;
if ((_a = aliasAction === null || aliasAction === void 0 ? void 0 : aliasAction.defaultParameters) === null || _a === void 0 ? void 0 : _a.length) {
const insertIndex = actionNameIndex + 1;
args = args.slice(0, insertIndex).concat(aliasAction.defaultParameters, args.slice(insertIndex));
}
}
}
const data = this._argumentParser.parseArgs(args);
this._processParsedData(this._options, data);
this.selectedAction = this.tryGetAction(data.action);
if (this.actions.length > 0 && !this.selectedAction) {
const actions = this.actions.map((x) => x.actionName);
throw new Error(`An action must be specified (${actions.join(', ')})`);
}
(_b = this.selectedAction) === null || _b === void 0 ? void 0 : _b._processParsedData(this._options, data);
await this.onExecute();
}
catch (err) {
if (err instanceof CommandLineParserExitError_1.CommandLineParserExitError) {
if (!err.exitCode) {
// non-error exit modeled using exception handling
if (err.message) {
// eslint-disable-next-line no-console
console.log(err.message);
}
return;
}
}
throw err;
}
}
/** @internal */
_registerDefinedParameters(state) {
super._registerDefinedParameters(state);
const { parentParameterNames } = state;
const updatedParentParameterNames = new Set([
...parentParameterNames,
...this._registeredParameterParserKeysByName.keys()
]);
const parentState = Object.assign(Object.assign({}, state), { parentParameterNames: updatedParentParameterNames });
for (const action of this._actions) {
action._registerDefinedParameters(parentState);
}
}
_validateDefinitions() {
if (this.remainder && this.actions.length > 0) {
// This is apparently not supported by argparse
throw new Error('defineCommandLineRemainder() cannot be called for a CommandLineParser with actions');
}
}
/**
* {@inheritDoc CommandLineParameterProvider._getArgumentParser}
* @internal
*/
_getArgumentParser() {
// override
return this._argumentParser;
}
/**
* This hook allows the subclass to perform additional operations before or after
* the chosen action is executed.
*/
async onExecute() {
if (this.selectedAction) {
await this.selectedAction._execute();
}
}
}
exports.CommandLineParser = CommandLineParser;
//# sourceMappingURL=CommandLineParser.js.map