From 66ed54818e730d37586d9fb6cf901e9e28f7f9ff Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Wed, 20 Apr 2022 14:37:34 -0700 Subject: [PATCH 01/73] Update argparse version Add new remainder test Add ScopedCommandLineParser Revert "Update argparse version" This reverts commit 6f917d351eede7297e65132dbb3f33a3622be6c3. Cleanup More cleanup Rush change Updated docstring --- ...de-ScopedCommandLine_2022-04-21-22-00.json | 10 + common/reviews/api/ts-command-line.api.md | 28 ++- libraries/ts-command-line/src/index.ts | 5 +- .../src/parameters/BaseClasses.ts | 4 + .../src/parameters/CommandLineDefinition.ts | 5 + .../src/providers/CommandLineAction.ts | 6 +- .../providers/CommandLineParameterProvider.ts | 80 ++++--- .../src/providers/CommandLineParser.ts | 13 +- .../src/providers/ScopedCommandLineAction.ts | 219 ++++++++++++++++++ .../src/test/CommandLineParameter.test.ts | 21 +- .../src/test/CommandLineRemainder.test.ts | 21 ++ .../src/test/ScopedCommandLineAction.test.ts | 112 +++++++++ .../CommandLineRemainder.test.ts.snap | 13 ++ .../ScopedCommandLineAction.test.ts.snap | 40 ++++ 14 files changed, 531 insertions(+), 46 deletions(-) create mode 100644 common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json create mode 100644 libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts create mode 100644 libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts create mode 100644 libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap diff --git a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json new file mode 100644 index 00000000000..d12bdde165b --- /dev/null +++ b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/ts-command-line", + "comment": "Add ScopedCommandLineAction class, which allows for the creation of actions that have variable arguments based on the provided scope.", + "type": "minor" + } + ], + "packageName": "@rushstack/ts-command-line" +} \ No newline at end of file diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index aea2b69eec2..6e00ee99f44 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -20,7 +20,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { protected abstract onDefineParameters(): void; protected abstract onExecute(): Promise; // @internal - _processParsedData(data: _ICommandLineParserData): void; + _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; readonly summary: string; } @@ -113,6 +113,7 @@ export abstract class CommandLineParameter { readonly environmentVariable: string | undefined; // @internal _getSupplementaryNotes(supplementaryNotes: string[]): void; + readonly groupName: string | undefined; abstract get kind(): CommandLineParameterKind; readonly longName: string; // @internal @@ -148,6 +149,8 @@ export abstract class CommandLineParameterProvider { defineFlagParameter(definition: ICommandLineFlagDefinition): CommandLineFlagParameter; defineIntegerListParameter(definition: ICommandLineIntegerListDefinition): CommandLineIntegerListParameter; defineIntegerParameter(definition: ICommandLineIntegerDefinition): CommandLineIntegerParameter; + // @internal (undocumented) + protected _defineParameter(parameter: CommandLineParameter): void; defineStringListParameter(definition: ICommandLineStringListDefinition): CommandLineStringListParameter; defineStringParameter(definition: ICommandLineStringDefinition): CommandLineStringParameter; // @internal @@ -164,9 +167,10 @@ export abstract class CommandLineParameterProvider { get parameters(): ReadonlyArray; get parametersProcessed(): boolean; // @internal (undocumented) - protected _processParsedData(data: _ICommandLineParserData): void; + protected _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; get remainder(): CommandLineRemainder | undefined; renderHelpText(): string; + renderUsageText(): string; } // @public @@ -249,6 +253,7 @@ export class DynamicCommandLineParser extends CommandLineParser { export interface IBaseCommandLineDefinition { description: string; environmentVariable?: string; + parameterGroupName?: string; parameterLongName: string; parameterShortName?: string; required?: boolean; @@ -306,6 +311,7 @@ export interface _ICommandLineParserData { export interface ICommandLineParserOptions { enableTabCompletionAction?: boolean; toolDescription: string; + toolEpilog?: string; toolFilename: string; } @@ -323,4 +329,22 @@ export interface ICommandLineStringDefinition extends IBaseCommandLineDefinition export interface ICommandLineStringListDefinition extends IBaseCommandLineDefinitionWithArgument { } +// @public +export abstract class ScopedCommandLineAction extends CommandLineAction { + constructor(options: ICommandLineActionOptions); + // @internal (undocumented) + protected _defineParameter(parameter: CommandLineParameter): void; + // @internal + _execute(): Promise; + // @internal + protected _getScopedCommandLineParser(): CommandLineParser; + protected onDefineParameters(): void; + protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; + protected abstract onDefineUnscopedParameters(): void; + protected abstract onExecute(): Promise; + // @internal + _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + static ScopingParameterGroupName: 'scoping'; +} + ``` diff --git a/libraries/ts-command-line/src/index.ts b/libraries/ts-command-line/src/index.ts index 2c82b011bc0..1cd47635a41 100644 --- a/libraries/ts-command-line/src/index.ts +++ b/libraries/ts-command-line/src/index.ts @@ -8,6 +8,8 @@ */ export { CommandLineAction, ICommandLineActionOptions } from './providers/CommandLineAction'; +export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; +export { ScopedCommandLineAction } from './providers/ScopedCommandLineAction'; export { IBaseCommandLineDefinition, @@ -43,9 +45,6 @@ export { } from './providers/CommandLineParameterProvider'; export { ICommandLineParserOptions, CommandLineParser } from './providers/CommandLineParser'; - -export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; - export { DynamicCommandLineParser } from './providers/DynamicCommandLineParser'; export { CommandLineConstants } from './Constants'; diff --git a/libraries/ts-command-line/src/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 8b61cb49a08..2c7c4e1f4d7 100644 --- a/libraries/ts-command-line/src/parameters/BaseClasses.ts +++ b/libraries/ts-command-line/src/parameters/BaseClasses.ts @@ -53,6 +53,9 @@ export abstract class CommandLineParameter { /** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */ public readonly shortName: string | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.parameterGroupName} */ + public readonly groupName: string | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.description} */ public readonly description: string; @@ -69,6 +72,7 @@ export abstract class CommandLineParameter { public constructor(definition: IBaseCommandLineDefinition) { this.longName = definition.parameterLongName; this.shortName = definition.parameterShortName; + this.groupName = definition.parameterGroupName; this.description = definition.description; this.required = !!definition.required; this.environmentVariable = definition.environmentVariable; diff --git a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts index 3d7913efb2c..04e5e72894a 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts @@ -17,6 +17,11 @@ export interface IBaseCommandLineDefinition { */ parameterShortName?: string; + /** + * An optional parameter group name, shown when invoking the tool with "--help" + */ + parameterGroupName?: string; + /** * Documentation for the parameter that will be shown when invoking the tool with "--help" */ diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index a0bd8deb4d9..e9f5fb874ea 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -2,7 +2,9 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; + import { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; +import type { ICommandLineParserOptions } from './CommandLineParser'; /** * Options for the CommandLineAction constructor. @@ -90,8 +92,8 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * This is called internally by CommandLineParser.execute() * @internal */ - public _processParsedData(data: ICommandLineParserData): void { - super._processParsedData(data); + public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + super._processParsedData(parserOptions, data); } /** diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 48e0818d01f..c25658e2f07 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -2,7 +2,8 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; -import { + +import type { ICommandLineChoiceDefinition, ICommandLineChoiceListDefinition, ICommandLineIntegerDefinition, @@ -12,6 +13,7 @@ import { ICommandLineStringListDefinition, ICommandLineRemainderDefinition } from '../parameters/CommandLineDefinition'; +import type { ICommandLineParserOptions } from './CommandLineParser'; import { CommandLineParameter, CommandLineParameterWithArgument, @@ -46,6 +48,7 @@ export abstract class CommandLineParameterProvider { private _parameters: CommandLineParameter[]; private _parametersByLongName: Map; + private _parameterGroupsByName: Map; private _parametersProcessed: boolean; private _remainder: CommandLineRemainder | undefined; @@ -54,6 +57,7 @@ export abstract class CommandLineParameterProvider { public constructor() { this._parameters = []; this._parametersByLongName = new Map(); + this._parameterGroupsByName = new Map(); this._parametersProcessed = false; } @@ -207,6 +211,7 @@ export abstract class CommandLineParameterProvider { public getIntegerListParameter(parameterLongName: string): CommandLineIntegerListParameter { return this._getParameter(parameterLongName, CommandLineParameterKind.IntegerList); } + /** * Defines a command-line parameter whose argument is a single text string. * @@ -297,6 +302,13 @@ export abstract class CommandLineParameterProvider { return this._getArgumentParser().formatHelp(); } + /** + * Generates the command-line usage text. + */ + public renderUsageText(): string { + return this._getArgumentParser().formatUsage(); + } + /** * Returns a object which maps the long name of each parameter in this.parameters * to the stringified form of its value. This is useful for logging telemetry, but @@ -349,7 +361,7 @@ export abstract class CommandLineParameterProvider { protected abstract _getArgumentParser(): argparse.ArgumentParser; /** @internal */ - protected _processParsedData(data: ICommandLineParserData): void { + protected _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { if (this._parametersProcessed) { throw new Error('Command Line Parser Data was already processed'); } @@ -367,28 +379,8 @@ export abstract class CommandLineParameterProvider { this._parametersProcessed = true; } - private _generateKey(): string { - return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); - } - - private _getParameter( - parameterLongName: string, - expectedKind: CommandLineParameterKind - ): T { - const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); - if (!parameter) { - throw new Error(`The parameter "${parameterLongName}" is not defined`); - } - if (parameter.kind !== expectedKind) { - throw new Error( - `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + - ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` - ); - } - return parameter as T; - } - - private _defineParameter(parameter: CommandLineParameter): void { + /** @internal */ + protected _defineParameter(parameter: CommandLineParameter): void { if (this._remainder) { throw new Error( 'defineCommandLineRemainder() was already called for this provider;' + @@ -455,10 +447,23 @@ export abstract class CommandLineParameterProvider { break; } - const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); - argumentParser.addArgument(names, { ...argparseOptions }); + let argumentGroup: argparse.ArgumentGroup | undefined; + if (parameter.groupName) { + argumentGroup = this._parameterGroupsByName.get(parameter.groupName); + if (!argumentGroup) { + argumentGroup = this._getArgumentParser().addArgumentGroup({ + title: `Optional ${parameter.groupName} arguments` + }); + this._parameterGroupsByName.set(parameter.groupName, argumentGroup); + } + } else { + argumentGroup = this._getArgumentParser(); + } + + argumentGroup.addArgument(names, { ...argparseOptions }); + if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { - argumentParser.addArgument(parameter.undocumentedSynonyms, { + argumentGroup.addArgument(parameter.undocumentedSynonyms, { ...argparseOptions, help: argparse.Const.SUPPRESS }); @@ -467,4 +472,25 @@ export abstract class CommandLineParameterProvider { this._parameters.push(parameter); this._parametersByLongName.set(parameter.longName, parameter); } + + private _generateKey(): string { + return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); + } + + private _getParameter( + parameterLongName: string, + expectedKind: CommandLineParameterKind + ): T { + const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); + if (!parameter) { + throw new Error(`The parameter "${parameterLongName}" is not defined`); + } + if (parameter.kind !== expectedKind) { + throw new Error( + `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + + ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` + ); + } + return parameter as T; + } } diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index f14888886ec..204f40e5b13 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -24,6 +24,12 @@ export interface ICommandLineParserOptions { */ toolDescription: string; + /** + * An optional string to append at the end of the "--help" main page. If not provided, an epilog + * will be automatically generated based on the toolFilename. + */ + toolEpilog?: string; + /** * Set to true to auto-define a tab completion action. False by default. */ @@ -68,7 +74,8 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( - `For detailed help about a specific command, use: ${this._options.toolFilename} -h` + this._options.toolEpilog ?? + `For detailed help about a specific command, use: ${this._options.toolFilename} -h` ) }); @@ -201,12 +208,12 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { const data: ICommandLineParserData = this._argumentParser.parseArgs(args); - this._processParsedData(data); + this._processParsedData(this._options, data); for (const action of this._actions) { if (action.actionName === data.action) { this.selectedAction = action; - action._processParsedData(data); + action._processParsedData(this._options, data); break; } } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts new file mode 100644 index 00000000000..6a713b84c89 --- /dev/null +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { CommandLineAction, ICommandLineActionOptions } from './CommandLineAction'; +import { CommandLineParser, ICommandLineParserOptions } from './CommandLineParser'; +import { CommandLineParserExitError } from './CommandLineParserExitError'; +import type { CommandLineParameter } from '../parameters/BaseClasses'; +import type { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; + +interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { + readonly actionOptions: ICommandLineActionOptions; + readonly unscopedActionParameters: ReadonlyArray; + readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; + readonly onExecute: () => Promise; +} + +class InternalScopedCommandLineParser extends CommandLineParser { + private _internalOptions: IInternalScopedCommandLineParserOptions; + + public constructor(options: IInternalScopedCommandLineParserOptions) { + // We can run the parser directly because we are not going to use it for any other actions, + // so construct a special options object to make the "--help" text more useful. + const scopingArgs: string[] = []; + for (const parameter of options.unscopedActionParameters) { + parameter.appendToArgList(scopingArgs); + } + const unscopedToolName: string = `${options.toolFilename} ${options.actionOptions.actionName}`; + const scopedCommandLineParserOptions: ICommandLineParserOptions = { + toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, + toolDescription: options.actionOptions.documentation, + toolEpilog: `For more information on available unscoped parameters, use "${unscopedToolName} --help"`, + enableTabCompletionAction: false + }; + + super(scopedCommandLineParserOptions); + this._internalOptions = options; + this._internalOptions.onDefineScopedParameters(this); + } + + protected onDefineParameters(): void { + // No-op. Parameters are manually defined in the constructor. + } + + protected onExecute(): Promise { + // Redirect action execution to the provided callback + return this._internalOptions.onExecute(); + } +} + +/** + * Represents a sub-command that is part of the CommandLineParser command-line. + * Applications should create subclasses of ScopedCommandLineAction corresponding to + * each action that they want to expose. + * + * The action name should be comprised of lower case words separated by hyphens + * or colons. The name should include an English verb (e.g. "deploy"). Use a + * hyphen to separate words (e.g. "upload-docs"). A group of related commands + * can be prefixed with a colon (e.g. "docs:generate", "docs:deploy", + * "docs:serve", etc). + * + * Scoped commands allow for different parameters to be specified for different + * provided scoping values. For example, the "scoped-action --scope A" command + * may allow for different scoped arguments to be specified than the "scoped-action + * --scope B" command. + * + * Scoped arguments are specified after the "--" pseudo-argument. For example, + * "scoped-action --scope A -- --scopedFoo --scopedBar". + * + * @public + */ +export abstract class ScopedCommandLineAction extends CommandLineAction { + private _options: ICommandLineActionOptions; + private _scopingParameters: CommandLineParameter[]; + private _unscopedParserOptions: ICommandLineParserOptions | undefined; + private _scopedCommandLineParser: InternalScopedCommandLineParser | undefined; + + /** + * The required group name to apply to all scoping parameters. At least one parameter + * must be defined with this group name. + */ + public static ScopingParameterGroupName: 'scoping' = 'scoping'; + + public constructor(options: ICommandLineActionOptions) { + super(options); + + this._options = options; + this._scopingParameters = []; + } + + /** + * {@inheritdoc CommandLineAction._processParsedData} + * @internal + */ + public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + // override + super._processParsedData(parserOptions, data); + + this._unscopedParserOptions = parserOptions; + + // Generate the scoped parser using the parent parser information. We can only create this after we + // have parsed the data, since the parameter values are used during construction. + this._scopedCommandLineParser = new InternalScopedCommandLineParser({ + ...parserOptions, + actionOptions: this._options, + unscopedActionParameters: this.parameters, + onDefineScopedParameters: this.onDefineScopedParameters.bind(this), + onExecute: this.onExecute.bind(this) + }); + } + + /** + * {@inheritdoc CommandLineAction._execute} + * @internal + */ + public async _execute(): Promise { + // override + if (!this._unscopedParserOptions || !this._scopedCommandLineParser) { + throw new Error('The CommandLineAction parameters must be processed before execution.'); + } + if (!this.remainder) { + throw new Error('CommandLineAction.onDefineParameters must be called before execution.'); + } + + // The '--' argument is required to separate the action parameters from the scoped parameters, + // so it needs to be trimmed. If remainder values are provided but no '--' is found, then throw. + const scopedArgs: string[] = []; + if (this.remainder.values.length) { + if (this.remainder.values[0] !== '--') { + // Immitate argparse behavior and log out usage text before throwing. + console.log(this.renderUsageText()); + throw new CommandLineParserExitError( + // argparse sets exit code 2 for invalid arguments + 2, + // model the message off of the built-in "unrecognized arguments" message + `${this._unscopedParserOptions.toolFilename} ${this.actionName}: error: Unrecognized ` + + `arguments: ${this.remainder.values[0]}.` + ); + } + scopedArgs.push(...this.remainder.values.slice(1)); + } + + // Call the scoped parser using only the scoped args. + await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + return; + } + + /** + * {@inheritdoc CommandLineParameterProvider.onDefineParameters} + */ + protected onDefineParameters(): void { + this.onDefineUnscopedParameters(); + + if (!this._scopingParameters.length) { + throw new Error( + 'No scoping parameters defined. At least one scoping parameter must be defined. ' + + 'Scoping parameters are defined by setting the parameterGroupName to ' + + 'ScopedCommandLineAction.ScopingParameterGroupName.' + ); + } + if (this.remainder) { + throw new Error( + 'Unscoped remainder parameters are not allowed. Remainder parameters can only be defined on ' + + 'the scoped parameter provider in onDefineScopedParameters().' + ); + } + + // Consume the remainder of the command-line, which will later be passed the scoped parser. + // This will also prevent developers from calling this.defineCommandLineRemainder(...) since + // we will have already defined it. + this.defineCommandLineRemainder({ + description: + 'Scoped parameters. Must be prefixed with "--", ex. "-- --scopedParameter ' + + 'foo --scopedFlag". For more information on available scoped parameters, use "-- --help".' + }); + } + + /** + * Retrieves the scoped CommandLineParser, which is populated after the ScopedCommandLineAction is executed. + * @internal + */ + protected _getScopedCommandLineParser(): CommandLineParser { + if (!this._scopedCommandLineParser) { + throw new Error('The scoped CommandLineParser is only populated after the action is executed.'); + } + return this._scopedCommandLineParser; + } + + /** @internal */ + protected _defineParameter(parameter: CommandLineParameter): void { + super._defineParameter(parameter); + if (parameter.groupName === ScopedCommandLineAction.ScopingParameterGroupName) { + this._scopingParameters.push(parameter); + } + } + + /** + * The child class should implement this hook to define its unscoped command-line parameters, + * e.g. by calling defineFlagParameter(). At least one scoping parameter must be defined. + * Scoping parameters are defined by setting the parameterGroupName to + * ScopedCommandLineAction.ScopingParameterGroupName. + */ + protected abstract onDefineUnscopedParameters(): void; + + /** + * The child class should implement this hook to define its scoped command-line + * parameters, e.g. by calling scopedParameterProvider.defineFlagParameter(). These + * parameters will only be available if the action is invoked with a scope. + * + * @remarks + * onDefineScopedParameters is called after the unscoped parameters have been parsed. + * The values they provide can be used to vary the defined scope parameters. + */ + protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; + + /** + * {@inheritDoc CommandLineAction.onExecute} + */ + protected abstract onExecute(): Promise; +} diff --git a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts index 08c104e9d90..82fe7ddc79d 100644 --- a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts @@ -361,31 +361,34 @@ describe(CommandLineParameter.name, () => { return commandLineParser; } - it('raises an error if env var value is not valid json', () => { + it('raises an error if env var value is not valid json', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[u'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + return expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is json containing non-scalars', () => { + it('raises an error if env var value is json containing non-scalars', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[{}]'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + return expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is not a valid choice', () => { + it('raises an error if env var value is not a valid choice', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = 'oblong'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + return expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts index 62bd8cdc949..0becb8dede7 100644 --- a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts @@ -73,4 +73,25 @@ describe(CommandLineRemainder.name, () => { expect(copiedArgs).toMatchSnapshot(); }); + + it('parses an action input with remainder flagged options', async () => { + const commandLineParser: CommandLineParser = createParser(); + const action: CommandLineAction = commandLineParser.getAction('run'); + const args: string[] = ['run', '--title', 'The title', '--', '--the', 'remaining', '--args']; + + await commandLineParser.execute(args); + + expect(commandLineParser.selectedAction).toBe(action); + + const copiedArgs: string[] = []; + for (const parameter of action.parameters) { + copiedArgs.push(`### ${parameter.longName} output: ###`); + parameter.appendToArgList(copiedArgs); + } + + copiedArgs.push(`### remainder output: ###`); + action.remainder!.appendToArgList(copiedArgs); + + expect(copiedArgs).toMatchSnapshot(); + }); }); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts new file mode 100644 index 00000000000..2caaaf3ffc5 --- /dev/null +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as colors from 'colors'; + +import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; +import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; +import { CommandLineParser } from '../providers/CommandLineParser'; +import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; + +class TestScopedAction extends ScopedCommandLineAction { + public done: boolean = false; + public scopedValue: string | undefined; + private _scopeArg!: CommandLineStringParameter; + private _scopedArg!: CommandLineStringParameter; + + public constructor() { + super({ + actionName: 'scoped-action', + summary: 'does the scoped action', + documentation: 'a longer description' + }); + } + + protected async onExecute(): Promise { + expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); + this.scopedValue = this._scopedArg.value; + this.done = true; + } + + protected onDefineUnscopedParameters(): void { + this._scopeArg = this.defineStringParameter({ + parameterLongName: '--scope', + parameterGroupName: ScopedCommandLineAction.ScopingParameterGroupName, + argumentName: 'SCOPE', + description: 'The scope' + }); + } + + protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { + this._scopedArg = scopedParameterProvider.defineStringParameter({ + parameterLongName: `--scoped-${this._scopeArg.value}`, + argumentName: 'SCOPED', + description: 'The scoped argument.' + }); + } +} + +class TestCommandLine extends CommandLineParser { + public constructor() { + super({ + toolFilename: 'example', + toolDescription: 'An example project' + }); + + this.addAction(new TestScopedAction()); + } + + protected onDefineParameters(): void { + // no parameters + } +} + +describe(CommandLineParser.name, () => { + it('throws on unknown scoped arg', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; + + return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('throws on missing positional arg divider with unknown positional args', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', 'bar']; + + return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('executes a scoped action', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + + expect(commandLineParser.selectedAction).toBeDefined(); + expect(commandLineParser.selectedAction!.actionName).toEqual('scoped-action'); + + const action: TestScopedAction = commandLineParser.selectedAction as TestScopedAction; + expect(action.done).toBe(true); + expect(action.scopedValue).toBe('bar'); + }); + + it('prints the action help', () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const helpText: string = colors.stripColors( + commandLineParser.getAction('scoped-action').renderHelpText() + ); + expect(helpText).toMatchSnapshot(); + }); + + it('prints the scoped action help', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + const unscopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = + commandLineParser.getAction('scoped-action') as TestScopedAction & { + _getScopedCommandLineParser(): CommandLineParser; + }; + const scopedCommandLineParser: CommandLineParser = unscopedAction._getScopedCommandLineParser(); + const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); + expect(helpText).toMatchSnapshot(); + }); +}); diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap index e11dc7a569d..1250aa3605e 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap @@ -12,6 +12,19 @@ Array [ ] `; +exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` +Array [ + "### --title output: ###", + "--title", + "The title", + "### remainder output: ###", + "--", + "--the", + "remaining", + "--args", +] +`; + exports[`CommandLineRemainder prints the action help 1`] = ` "usage: example run [-h] [--title TEXT] ... diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap new file mode 100644 index 00000000000..3179eb4c50d --- /dev/null +++ b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommandLineParser prints the action help 1`] = ` +"usage: example scoped-action [-h] [--scope SCOPE] ... + +a longer description + +Positional arguments: + \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- + --scopedParameter foo --scopedFlag\\". For more information on + available scoped parameters, use \\"-- --help\\". + +Optional arguments: + -h, --help Show this help message and exit. + +Optional scoping arguments: + --scope SCOPE The scope +" +`; + +exports[`CommandLineParser prints the scoped action help 1`] = ` +"usage: example scoped-action --scope foo -- [-h] [--scoped-foo SCOPED] + +a longer description + +Optional arguments: + -h, --help Show this help message and exit. + --scoped-foo SCOPED The scoped argument. + +For more information on available unscoped parameters, use \\"example +scoped-action --help\\" +" +`; + +exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; + +exports[`CommandLineParser throws on unknown scoped arg 1`] = ` +"example scoped-action --scope foo --: error: Unrecognized arguments: --scoped-bar baz. +" +`; From 117770eae717cb730c1a0ca02e179cc3ff04c98c Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Mon, 25 Apr 2022 13:45:11 -0700 Subject: [PATCH 02/73] Tweaks to allow for unscoped calls to scoping actions, allowing the developer to decide if the scope is required --- common/reviews/api/ts-command-line.api.md | 1 + .../src/providers/CommandLineParser.ts | 4 +- .../src/providers/ScopedCommandLineAction.ts | 16 ++++- .../src/test/ActionlessParser.test.ts | 18 ++++++ .../src/test/ScopedCommandLineAction.test.ts | 62 ++++++++++++++++--- .../ScopedCommandLineAction.test.ts.snap | 26 +++++++- 6 files changed, 113 insertions(+), 14 deletions(-) diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index 6e00ee99f44..55425e6b97f 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -342,6 +342,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; protected abstract onDefineUnscopedParameters(): void; protected abstract onExecute(): Promise; + get parameters(): ReadonlyArray; // @internal _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; static ScopingParameterGroupName: 'scoping'; diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index 204f40e5b13..25fc95342b1 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -201,7 +201,9 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { // 0=node.exe, 1=script name args = process.argv.slice(2); } - if (args.length === 0) { + if (this.actions.length > 0 && 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; } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index 6a713b84c89..53b35819160 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -41,9 +41,10 @@ class InternalScopedCommandLineParser extends CommandLineParser { // No-op. Parameters are manually defined in the constructor. } - protected onExecute(): Promise { + protected async onExecute(): Promise { + await super.onExecute(); // Redirect action execution to the provided callback - return this._internalOptions.onExecute(); + await this._internalOptions.onExecute(); } } @@ -87,6 +88,17 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { this._scopingParameters = []; } + /** + * {@inheritDoc CommandLineParameterProvider.parameters} + */ + public get parameters(): ReadonlyArray { + if (this._scopedCommandLineParser) { + return [...super.parameters, ...this._scopedCommandLineParser.parameters]; + } else { + return super.parameters; + } + } + /** * {@inheritdoc CommandLineAction._processParsedData} * @internal diff --git a/libraries/ts-command-line/src/test/ActionlessParser.test.ts b/libraries/ts-command-line/src/test/ActionlessParser.test.ts index dea5cdc0a64..71c0e7edd16 100644 --- a/libraries/ts-command-line/src/test/ActionlessParser.test.ts +++ b/libraries/ts-command-line/src/test/ActionlessParser.test.ts @@ -6,6 +6,7 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter class TestCommandLine extends CommandLineParser { public flag!: CommandLineFlagParameter; + public done: boolean = false; public constructor() { super({ @@ -14,6 +15,11 @@ class TestCommandLine extends CommandLineParser { }); } + protected async onExecute(): Promise { + await super.onExecute(); + this.done = true; + } + protected onDefineParameters(): void { this.flag = this.defineFlagParameter({ parameterLongName: '--flag', @@ -23,11 +29,22 @@ class TestCommandLine extends CommandLineParser { } describe(`Actionless ${CommandLineParser.name}`, () => { + it('parses an empty arg list', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + + await commandLineParser.execute([]); + + expect(commandLineParser.done).toBe(true); + expect(commandLineParser.selectedAction).toBeUndefined(); + expect(commandLineParser.flag.value).toBe(false); + }); + it('parses a flag', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); await commandLineParser.execute(['--flag']); + expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); }); @@ -41,6 +58,7 @@ describe(`Actionless ${CommandLineParser.name}`, () => { await commandLineParser.execute(['--flag', 'the', 'remaining', 'args']); + expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); expect(commandLineParser.remainder!.values).toEqual(['the', 'remaining', 'args']); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts index 2caaaf3ffc5..6998dcd565f 100644 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -7,12 +7,14 @@ import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; import { CommandLineParser } from '../providers/CommandLineParser'; import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; +import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter'; class TestScopedAction extends ScopedCommandLineAction { public done: boolean = false; public scopedValue: string | undefined; + private _verboseArg!: CommandLineFlagParameter; private _scopeArg!: CommandLineStringParameter; - private _scopedArg!: CommandLineStringParameter; + private _scopedArg: CommandLineStringParameter | undefined; public constructor() { super({ @@ -23,12 +25,19 @@ class TestScopedAction extends ScopedCommandLineAction { } protected async onExecute(): Promise { - expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); - this.scopedValue = this._scopedArg.value; + if (this._scopedArg) { + expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); + this.scopedValue = this._scopedArg.value; + } this.done = true; } protected onDefineUnscopedParameters(): void { + this._verboseArg = this.defineFlagParameter({ + parameterLongName: '--verbose', + description: 'A flag parameter.' + }); + this._scopeArg = this.defineStringParameter({ parameterLongName: '--scope', parameterGroupName: ScopedCommandLineAction.ScopingParameterGroupName, @@ -38,11 +47,13 @@ class TestScopedAction extends ScopedCommandLineAction { } protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { - this._scopedArg = scopedParameterProvider.defineStringParameter({ - parameterLongName: `--scoped-${this._scopeArg.value}`, - argumentName: 'SCOPED', - description: 'The scoped argument.' - }); + if (this._scopeArg.value) { + this._scopedArg = scopedParameterProvider.defineStringParameter({ + parameterLongName: `--scoped-${this._scopeArg.value}`, + argumentName: 'SCOPED', + description: 'The scoped argument.' + }); + } } } @@ -101,12 +112,43 @@ describe(CommandLineParser.name, () => { const commandLineParser: TestCommandLine = new TestCommandLine(); // Execute the parser in order to populate the scoped action await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - const unscopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = + const scopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = commandLineParser.getAction('scoped-action') as TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser; }; - const scopedCommandLineParser: CommandLineParser = unscopedAction._getScopedCommandLineParser(); + const scopedCommandLineParser: CommandLineParser = scopedAction._getScopedCommandLineParser(); const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); expect(helpText).toMatchSnapshot(); }); + + it('prints the unscoped action parameter map', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--verbose']); + const scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(2); + const parameterStringMap: Record = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + }); + + it('prints the scoped action parameter map', async () => { + let commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo']); + let scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(3); + let parameterStringMap: Record = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + + commandLineParser = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + scopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(3); + parameterStringMap = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + }); }); diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap index 3179eb4c50d..ca696e05d3d 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CommandLineParser prints the action help 1`] = ` -"usage: example scoped-action [-h] [--scope SCOPE] ... +"usage: example scoped-action [-h] [--verbose] [--scope SCOPE] ... a longer description @@ -12,6 +12,7 @@ Positional arguments: Optional arguments: -h, --help Show this help message and exit. + --verbose A flag parameter. Optional scoping arguments: --scope SCOPE The scope @@ -32,6 +33,29 @@ scoped-action --help\\" " `; +exports[`CommandLineParser prints the scoped action parameter map 1`] = ` +Object { + "--scope": "\\"foo\\"", + "--scoped-foo": undefined, + "--verbose": "false", +} +`; + +exports[`CommandLineParser prints the scoped action parameter map 2`] = ` +Object { + "--scope": "\\"foo\\"", + "--scoped-foo": "\\"bar\\"", + "--verbose": "false", +} +`; + +exports[`CommandLineParser prints the unscoped action parameter map 1`] = ` +Object { + "--scope": undefined, + "--verbose": "true", +} +`; + exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; exports[`CommandLineParser throws on unknown scoped arg 1`] = ` From 8bdfd129da47a18870a5fee646f522f92d8bf5c6 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 19:42:33 -0700 Subject: [PATCH 03/73] Revert "Tweaks to allow for unscoped calls to scoping actions, allowing the developer to decide if the scope is required" This reverts commit 117770eae717cb730c1a0ca02e179cc3ff04c98c. --- common/reviews/api/ts-command-line.api.md | 1 - .../src/providers/CommandLineParser.ts | 4 +- .../src/providers/ScopedCommandLineAction.ts | 16 +---- .../src/test/ActionlessParser.test.ts | 18 ------ .../src/test/ScopedCommandLineAction.test.ts | 62 +++---------------- .../ScopedCommandLineAction.test.ts.snap | 26 +------- 6 files changed, 14 insertions(+), 113 deletions(-) diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index 55425e6b97f..6e00ee99f44 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -342,7 +342,6 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; protected abstract onDefineUnscopedParameters(): void; protected abstract onExecute(): Promise; - get parameters(): ReadonlyArray; // @internal _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; static ScopingParameterGroupName: 'scoping'; diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index 25fc95342b1..204f40e5b13 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -201,9 +201,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { // 0=node.exe, 1=script name args = process.argv.slice(2); } - if (this.actions.length > 0 && args.length === 0) { - // Parsers that use actions should print help when 0 args are provided. Allow - // actionless parsers to continue on zero args. + if (args.length === 0) { this._argumentParser.printHelp(); return; } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index 53b35819160..6a713b84c89 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -41,10 +41,9 @@ class InternalScopedCommandLineParser extends CommandLineParser { // No-op. Parameters are manually defined in the constructor. } - protected async onExecute(): Promise { - await super.onExecute(); + protected onExecute(): Promise { // Redirect action execution to the provided callback - await this._internalOptions.onExecute(); + return this._internalOptions.onExecute(); } } @@ -88,17 +87,6 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { this._scopingParameters = []; } - /** - * {@inheritDoc CommandLineParameterProvider.parameters} - */ - public get parameters(): ReadonlyArray { - if (this._scopedCommandLineParser) { - return [...super.parameters, ...this._scopedCommandLineParser.parameters]; - } else { - return super.parameters; - } - } - /** * {@inheritdoc CommandLineAction._processParsedData} * @internal diff --git a/libraries/ts-command-line/src/test/ActionlessParser.test.ts b/libraries/ts-command-line/src/test/ActionlessParser.test.ts index 71c0e7edd16..dea5cdc0a64 100644 --- a/libraries/ts-command-line/src/test/ActionlessParser.test.ts +++ b/libraries/ts-command-line/src/test/ActionlessParser.test.ts @@ -6,7 +6,6 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter class TestCommandLine extends CommandLineParser { public flag!: CommandLineFlagParameter; - public done: boolean = false; public constructor() { super({ @@ -15,11 +14,6 @@ class TestCommandLine extends CommandLineParser { }); } - protected async onExecute(): Promise { - await super.onExecute(); - this.done = true; - } - protected onDefineParameters(): void { this.flag = this.defineFlagParameter({ parameterLongName: '--flag', @@ -29,22 +23,11 @@ class TestCommandLine extends CommandLineParser { } describe(`Actionless ${CommandLineParser.name}`, () => { - it('parses an empty arg list', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - - await commandLineParser.execute([]); - - expect(commandLineParser.done).toBe(true); - expect(commandLineParser.selectedAction).toBeUndefined(); - expect(commandLineParser.flag.value).toBe(false); - }); - it('parses a flag', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); await commandLineParser.execute(['--flag']); - expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); }); @@ -58,7 +41,6 @@ describe(`Actionless ${CommandLineParser.name}`, () => { await commandLineParser.execute(['--flag', 'the', 'remaining', 'args']); - expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); expect(commandLineParser.remainder!.values).toEqual(['the', 'remaining', 'args']); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts index 6998dcd565f..2caaaf3ffc5 100644 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -7,14 +7,12 @@ import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; import { CommandLineParser } from '../providers/CommandLineParser'; import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; -import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter'; class TestScopedAction extends ScopedCommandLineAction { public done: boolean = false; public scopedValue: string | undefined; - private _verboseArg!: CommandLineFlagParameter; private _scopeArg!: CommandLineStringParameter; - private _scopedArg: CommandLineStringParameter | undefined; + private _scopedArg!: CommandLineStringParameter; public constructor() { super({ @@ -25,19 +23,12 @@ class TestScopedAction extends ScopedCommandLineAction { } protected async onExecute(): Promise { - if (this._scopedArg) { - expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); - this.scopedValue = this._scopedArg.value; - } + expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); + this.scopedValue = this._scopedArg.value; this.done = true; } protected onDefineUnscopedParameters(): void { - this._verboseArg = this.defineFlagParameter({ - parameterLongName: '--verbose', - description: 'A flag parameter.' - }); - this._scopeArg = this.defineStringParameter({ parameterLongName: '--scope', parameterGroupName: ScopedCommandLineAction.ScopingParameterGroupName, @@ -47,13 +38,11 @@ class TestScopedAction extends ScopedCommandLineAction { } protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { - if (this._scopeArg.value) { - this._scopedArg = scopedParameterProvider.defineStringParameter({ - parameterLongName: `--scoped-${this._scopeArg.value}`, - argumentName: 'SCOPED', - description: 'The scoped argument.' - }); - } + this._scopedArg = scopedParameterProvider.defineStringParameter({ + parameterLongName: `--scoped-${this._scopeArg.value}`, + argumentName: 'SCOPED', + description: 'The scoped argument.' + }); } } @@ -112,43 +101,12 @@ describe(CommandLineParser.name, () => { const commandLineParser: TestCommandLine = new TestCommandLine(); // Execute the parser in order to populate the scoped action await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - const scopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = + const unscopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = commandLineParser.getAction('scoped-action') as TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser; }; - const scopedCommandLineParser: CommandLineParser = scopedAction._getScopedCommandLineParser(); + const scopedCommandLineParser: CommandLineParser = unscopedAction._getScopedCommandLineParser(); const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); expect(helpText).toMatchSnapshot(); }); - - it('prints the unscoped action parameter map', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--verbose']); - const scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; - expect(scopedAction.done).toBe(true); - expect(scopedAction.parameters.length).toBe(2); - const parameterStringMap: Record = scopedAction.getParameterStringMap(); - expect(parameterStringMap).toMatchSnapshot(); - }); - - it('prints the scoped action parameter map', async () => { - let commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--scope', 'foo']); - let scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; - expect(scopedAction.done).toBe(true); - expect(scopedAction.parameters.length).toBe(3); - let parameterStringMap: Record = scopedAction.getParameterStringMap(); - expect(parameterStringMap).toMatchSnapshot(); - - commandLineParser = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - scopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; - expect(scopedAction.done).toBe(true); - expect(scopedAction.parameters.length).toBe(3); - parameterStringMap = scopedAction.getParameterStringMap(); - expect(parameterStringMap).toMatchSnapshot(); - }); }); diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap index ca696e05d3d..3179eb4c50d 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CommandLineParser prints the action help 1`] = ` -"usage: example scoped-action [-h] [--verbose] [--scope SCOPE] ... +"usage: example scoped-action [-h] [--scope SCOPE] ... a longer description @@ -12,7 +12,6 @@ Positional arguments: Optional arguments: -h, --help Show this help message and exit. - --verbose A flag parameter. Optional scoping arguments: --scope SCOPE The scope @@ -33,29 +32,6 @@ scoped-action --help\\" " `; -exports[`CommandLineParser prints the scoped action parameter map 1`] = ` -Object { - "--scope": "\\"foo\\"", - "--scoped-foo": undefined, - "--verbose": "false", -} -`; - -exports[`CommandLineParser prints the scoped action parameter map 2`] = ` -Object { - "--scope": "\\"foo\\"", - "--scoped-foo": "\\"bar\\"", - "--verbose": "false", -} -`; - -exports[`CommandLineParser prints the unscoped action parameter map 1`] = ` -Object { - "--scope": undefined, - "--verbose": "true", -} -`; - exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; exports[`CommandLineParser throws on unknown scoped arg 1`] = ` From b5a8e156ca5dfcf13487f72f0512d73faa97c455 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 19:42:48 -0700 Subject: [PATCH 04/73] Revert "Update argparse version" This reverts commit 66ed54818e730d37586d9fb6cf901e9e28f7f9ff. --- ...de-ScopedCommandLine_2022-04-21-22-00.json | 10 - common/reviews/api/ts-command-line.api.md | 28 +-- libraries/ts-command-line/src/index.ts | 5 +- .../src/parameters/BaseClasses.ts | 4 - .../src/parameters/CommandLineDefinition.ts | 5 - .../src/providers/CommandLineAction.ts | 6 +- .../providers/CommandLineParameterProvider.ts | 80 +++---- .../src/providers/CommandLineParser.ts | 13 +- .../src/providers/ScopedCommandLineAction.ts | 219 ------------------ .../src/test/CommandLineParameter.test.ts | 21 +- .../src/test/CommandLineRemainder.test.ts | 21 -- .../src/test/ScopedCommandLineAction.test.ts | 112 --------- .../CommandLineRemainder.test.ts.snap | 13 -- .../ScopedCommandLineAction.test.ts.snap | 40 ---- 14 files changed, 46 insertions(+), 531 deletions(-) delete mode 100644 common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json delete mode 100644 libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts delete mode 100644 libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts delete mode 100644 libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap diff --git a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json deleted file mode 100644 index d12bdde165b..00000000000 --- a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@rushstack/ts-command-line", - "comment": "Add ScopedCommandLineAction class, which allows for the creation of actions that have variable arguments based on the provided scope.", - "type": "minor" - } - ], - "packageName": "@rushstack/ts-command-line" -} \ No newline at end of file diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index 6e00ee99f44..aea2b69eec2 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -20,7 +20,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { protected abstract onDefineParameters(): void; protected abstract onExecute(): Promise; // @internal - _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + _processParsedData(data: _ICommandLineParserData): void; readonly summary: string; } @@ -113,7 +113,6 @@ export abstract class CommandLineParameter { readonly environmentVariable: string | undefined; // @internal _getSupplementaryNotes(supplementaryNotes: string[]): void; - readonly groupName: string | undefined; abstract get kind(): CommandLineParameterKind; readonly longName: string; // @internal @@ -149,8 +148,6 @@ export abstract class CommandLineParameterProvider { defineFlagParameter(definition: ICommandLineFlagDefinition): CommandLineFlagParameter; defineIntegerListParameter(definition: ICommandLineIntegerListDefinition): CommandLineIntegerListParameter; defineIntegerParameter(definition: ICommandLineIntegerDefinition): CommandLineIntegerParameter; - // @internal (undocumented) - protected _defineParameter(parameter: CommandLineParameter): void; defineStringListParameter(definition: ICommandLineStringListDefinition): CommandLineStringListParameter; defineStringParameter(definition: ICommandLineStringDefinition): CommandLineStringParameter; // @internal @@ -167,10 +164,9 @@ export abstract class CommandLineParameterProvider { get parameters(): ReadonlyArray; get parametersProcessed(): boolean; // @internal (undocumented) - protected _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + protected _processParsedData(data: _ICommandLineParserData): void; get remainder(): CommandLineRemainder | undefined; renderHelpText(): string; - renderUsageText(): string; } // @public @@ -253,7 +249,6 @@ export class DynamicCommandLineParser extends CommandLineParser { export interface IBaseCommandLineDefinition { description: string; environmentVariable?: string; - parameterGroupName?: string; parameterLongName: string; parameterShortName?: string; required?: boolean; @@ -311,7 +306,6 @@ export interface _ICommandLineParserData { export interface ICommandLineParserOptions { enableTabCompletionAction?: boolean; toolDescription: string; - toolEpilog?: string; toolFilename: string; } @@ -329,22 +323,4 @@ export interface ICommandLineStringDefinition extends IBaseCommandLineDefinition export interface ICommandLineStringListDefinition extends IBaseCommandLineDefinitionWithArgument { } -// @public -export abstract class ScopedCommandLineAction extends CommandLineAction { - constructor(options: ICommandLineActionOptions); - // @internal (undocumented) - protected _defineParameter(parameter: CommandLineParameter): void; - // @internal - _execute(): Promise; - // @internal - protected _getScopedCommandLineParser(): CommandLineParser; - protected onDefineParameters(): void; - protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; - protected abstract onDefineUnscopedParameters(): void; - protected abstract onExecute(): Promise; - // @internal - _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; - static ScopingParameterGroupName: 'scoping'; -} - ``` diff --git a/libraries/ts-command-line/src/index.ts b/libraries/ts-command-line/src/index.ts index 1cd47635a41..2c82b011bc0 100644 --- a/libraries/ts-command-line/src/index.ts +++ b/libraries/ts-command-line/src/index.ts @@ -8,8 +8,6 @@ */ export { CommandLineAction, ICommandLineActionOptions } from './providers/CommandLineAction'; -export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; -export { ScopedCommandLineAction } from './providers/ScopedCommandLineAction'; export { IBaseCommandLineDefinition, @@ -45,6 +43,9 @@ export { } from './providers/CommandLineParameterProvider'; export { ICommandLineParserOptions, CommandLineParser } from './providers/CommandLineParser'; + +export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; + export { DynamicCommandLineParser } from './providers/DynamicCommandLineParser'; export { CommandLineConstants } from './Constants'; diff --git a/libraries/ts-command-line/src/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 2c7c4e1f4d7..8b61cb49a08 100644 --- a/libraries/ts-command-line/src/parameters/BaseClasses.ts +++ b/libraries/ts-command-line/src/parameters/BaseClasses.ts @@ -53,9 +53,6 @@ export abstract class CommandLineParameter { /** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */ public readonly shortName: string | undefined; - /** {@inheritDoc IBaseCommandLineDefinition.parameterGroupName} */ - public readonly groupName: string | undefined; - /** {@inheritDoc IBaseCommandLineDefinition.description} */ public readonly description: string; @@ -72,7 +69,6 @@ export abstract class CommandLineParameter { public constructor(definition: IBaseCommandLineDefinition) { this.longName = definition.parameterLongName; this.shortName = definition.parameterShortName; - this.groupName = definition.parameterGroupName; this.description = definition.description; this.required = !!definition.required; this.environmentVariable = definition.environmentVariable; diff --git a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts index 04e5e72894a..3d7913efb2c 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts @@ -17,11 +17,6 @@ export interface IBaseCommandLineDefinition { */ parameterShortName?: string; - /** - * An optional parameter group name, shown when invoking the tool with "--help" - */ - parameterGroupName?: string; - /** * Documentation for the parameter that will be shown when invoking the tool with "--help" */ diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index e9f5fb874ea..a0bd8deb4d9 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -2,9 +2,7 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; - import { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; -import type { ICommandLineParserOptions } from './CommandLineParser'; /** * Options for the CommandLineAction constructor. @@ -92,8 +90,8 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * This is called internally by CommandLineParser.execute() * @internal */ - public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { - super._processParsedData(parserOptions, data); + public _processParsedData(data: ICommandLineParserData): void { + super._processParsedData(data); } /** diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index c25658e2f07..48e0818d01f 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -2,8 +2,7 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; - -import type { +import { ICommandLineChoiceDefinition, ICommandLineChoiceListDefinition, ICommandLineIntegerDefinition, @@ -13,7 +12,6 @@ import type { ICommandLineStringListDefinition, ICommandLineRemainderDefinition } from '../parameters/CommandLineDefinition'; -import type { ICommandLineParserOptions } from './CommandLineParser'; import { CommandLineParameter, CommandLineParameterWithArgument, @@ -48,7 +46,6 @@ export abstract class CommandLineParameterProvider { private _parameters: CommandLineParameter[]; private _parametersByLongName: Map; - private _parameterGroupsByName: Map; private _parametersProcessed: boolean; private _remainder: CommandLineRemainder | undefined; @@ -57,7 +54,6 @@ export abstract class CommandLineParameterProvider { public constructor() { this._parameters = []; this._parametersByLongName = new Map(); - this._parameterGroupsByName = new Map(); this._parametersProcessed = false; } @@ -211,7 +207,6 @@ export abstract class CommandLineParameterProvider { public getIntegerListParameter(parameterLongName: string): CommandLineIntegerListParameter { return this._getParameter(parameterLongName, CommandLineParameterKind.IntegerList); } - /** * Defines a command-line parameter whose argument is a single text string. * @@ -302,13 +297,6 @@ export abstract class CommandLineParameterProvider { return this._getArgumentParser().formatHelp(); } - /** - * Generates the command-line usage text. - */ - public renderUsageText(): string { - return this._getArgumentParser().formatUsage(); - } - /** * Returns a object which maps the long name of each parameter in this.parameters * to the stringified form of its value. This is useful for logging telemetry, but @@ -361,7 +349,7 @@ export abstract class CommandLineParameterProvider { protected abstract _getArgumentParser(): argparse.ArgumentParser; /** @internal */ - protected _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + protected _processParsedData(data: ICommandLineParserData): void { if (this._parametersProcessed) { throw new Error('Command Line Parser Data was already processed'); } @@ -379,8 +367,28 @@ export abstract class CommandLineParameterProvider { this._parametersProcessed = true; } - /** @internal */ - protected _defineParameter(parameter: CommandLineParameter): void { + private _generateKey(): string { + return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); + } + + private _getParameter( + parameterLongName: string, + expectedKind: CommandLineParameterKind + ): T { + const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); + if (!parameter) { + throw new Error(`The parameter "${parameterLongName}" is not defined`); + } + if (parameter.kind !== expectedKind) { + throw new Error( + `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + + ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` + ); + } + return parameter as T; + } + + private _defineParameter(parameter: CommandLineParameter): void { if (this._remainder) { throw new Error( 'defineCommandLineRemainder() was already called for this provider;' + @@ -447,23 +455,10 @@ export abstract class CommandLineParameterProvider { break; } - let argumentGroup: argparse.ArgumentGroup | undefined; - if (parameter.groupName) { - argumentGroup = this._parameterGroupsByName.get(parameter.groupName); - if (!argumentGroup) { - argumentGroup = this._getArgumentParser().addArgumentGroup({ - title: `Optional ${parameter.groupName} arguments` - }); - this._parameterGroupsByName.set(parameter.groupName, argumentGroup); - } - } else { - argumentGroup = this._getArgumentParser(); - } - - argumentGroup.addArgument(names, { ...argparseOptions }); - + const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); + argumentParser.addArgument(names, { ...argparseOptions }); if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { - argumentGroup.addArgument(parameter.undocumentedSynonyms, { + argumentParser.addArgument(parameter.undocumentedSynonyms, { ...argparseOptions, help: argparse.Const.SUPPRESS }); @@ -472,25 +467,4 @@ export abstract class CommandLineParameterProvider { this._parameters.push(parameter); this._parametersByLongName.set(parameter.longName, parameter); } - - private _generateKey(): string { - return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); - } - - private _getParameter( - parameterLongName: string, - expectedKind: CommandLineParameterKind - ): T { - const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); - if (!parameter) { - throw new Error(`The parameter "${parameterLongName}" is not defined`); - } - if (parameter.kind !== expectedKind) { - throw new Error( - `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + - ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` - ); - } - return parameter as T; - } } diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index 204f40e5b13..f14888886ec 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -24,12 +24,6 @@ export interface ICommandLineParserOptions { */ toolDescription: string; - /** - * An optional string to append at the end of the "--help" main page. If not provided, an epilog - * will be automatically generated based on the toolFilename. - */ - toolEpilog?: string; - /** * Set to true to auto-define a tab completion action. False by default. */ @@ -74,8 +68,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( - this._options.toolEpilog ?? - `For detailed help about a specific command, use: ${this._options.toolFilename} -h` + `For detailed help about a specific command, use: ${this._options.toolFilename} -h` ) }); @@ -208,12 +201,12 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { const data: ICommandLineParserData = this._argumentParser.parseArgs(args); - this._processParsedData(this._options, data); + this._processParsedData(data); for (const action of this._actions) { if (action.actionName === data.action) { this.selectedAction = action; - action._processParsedData(this._options, data); + action._processParsedData(data); break; } } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts deleted file mode 100644 index 6a713b84c89..00000000000 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { CommandLineAction, ICommandLineActionOptions } from './CommandLineAction'; -import { CommandLineParser, ICommandLineParserOptions } from './CommandLineParser'; -import { CommandLineParserExitError } from './CommandLineParserExitError'; -import type { CommandLineParameter } from '../parameters/BaseClasses'; -import type { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; - -interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { - readonly actionOptions: ICommandLineActionOptions; - readonly unscopedActionParameters: ReadonlyArray; - readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; - readonly onExecute: () => Promise; -} - -class InternalScopedCommandLineParser extends CommandLineParser { - private _internalOptions: IInternalScopedCommandLineParserOptions; - - public constructor(options: IInternalScopedCommandLineParserOptions) { - // We can run the parser directly because we are not going to use it for any other actions, - // so construct a special options object to make the "--help" text more useful. - const scopingArgs: string[] = []; - for (const parameter of options.unscopedActionParameters) { - parameter.appendToArgList(scopingArgs); - } - const unscopedToolName: string = `${options.toolFilename} ${options.actionOptions.actionName}`; - const scopedCommandLineParserOptions: ICommandLineParserOptions = { - toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, - toolDescription: options.actionOptions.documentation, - toolEpilog: `For more information on available unscoped parameters, use "${unscopedToolName} --help"`, - enableTabCompletionAction: false - }; - - super(scopedCommandLineParserOptions); - this._internalOptions = options; - this._internalOptions.onDefineScopedParameters(this); - } - - protected onDefineParameters(): void { - // No-op. Parameters are manually defined in the constructor. - } - - protected onExecute(): Promise { - // Redirect action execution to the provided callback - return this._internalOptions.onExecute(); - } -} - -/** - * Represents a sub-command that is part of the CommandLineParser command-line. - * Applications should create subclasses of ScopedCommandLineAction corresponding to - * each action that they want to expose. - * - * The action name should be comprised of lower case words separated by hyphens - * or colons. The name should include an English verb (e.g. "deploy"). Use a - * hyphen to separate words (e.g. "upload-docs"). A group of related commands - * can be prefixed with a colon (e.g. "docs:generate", "docs:deploy", - * "docs:serve", etc). - * - * Scoped commands allow for different parameters to be specified for different - * provided scoping values. For example, the "scoped-action --scope A" command - * may allow for different scoped arguments to be specified than the "scoped-action - * --scope B" command. - * - * Scoped arguments are specified after the "--" pseudo-argument. For example, - * "scoped-action --scope A -- --scopedFoo --scopedBar". - * - * @public - */ -export abstract class ScopedCommandLineAction extends CommandLineAction { - private _options: ICommandLineActionOptions; - private _scopingParameters: CommandLineParameter[]; - private _unscopedParserOptions: ICommandLineParserOptions | undefined; - private _scopedCommandLineParser: InternalScopedCommandLineParser | undefined; - - /** - * The required group name to apply to all scoping parameters. At least one parameter - * must be defined with this group name. - */ - public static ScopingParameterGroupName: 'scoping' = 'scoping'; - - public constructor(options: ICommandLineActionOptions) { - super(options); - - this._options = options; - this._scopingParameters = []; - } - - /** - * {@inheritdoc CommandLineAction._processParsedData} - * @internal - */ - public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { - // override - super._processParsedData(parserOptions, data); - - this._unscopedParserOptions = parserOptions; - - // Generate the scoped parser using the parent parser information. We can only create this after we - // have parsed the data, since the parameter values are used during construction. - this._scopedCommandLineParser = new InternalScopedCommandLineParser({ - ...parserOptions, - actionOptions: this._options, - unscopedActionParameters: this.parameters, - onDefineScopedParameters: this.onDefineScopedParameters.bind(this), - onExecute: this.onExecute.bind(this) - }); - } - - /** - * {@inheritdoc CommandLineAction._execute} - * @internal - */ - public async _execute(): Promise { - // override - if (!this._unscopedParserOptions || !this._scopedCommandLineParser) { - throw new Error('The CommandLineAction parameters must be processed before execution.'); - } - if (!this.remainder) { - throw new Error('CommandLineAction.onDefineParameters must be called before execution.'); - } - - // The '--' argument is required to separate the action parameters from the scoped parameters, - // so it needs to be trimmed. If remainder values are provided but no '--' is found, then throw. - const scopedArgs: string[] = []; - if (this.remainder.values.length) { - if (this.remainder.values[0] !== '--') { - // Immitate argparse behavior and log out usage text before throwing. - console.log(this.renderUsageText()); - throw new CommandLineParserExitError( - // argparse sets exit code 2 for invalid arguments - 2, - // model the message off of the built-in "unrecognized arguments" message - `${this._unscopedParserOptions.toolFilename} ${this.actionName}: error: Unrecognized ` + - `arguments: ${this.remainder.values[0]}.` - ); - } - scopedArgs.push(...this.remainder.values.slice(1)); - } - - // Call the scoped parser using only the scoped args. - await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); - return; - } - - /** - * {@inheritdoc CommandLineParameterProvider.onDefineParameters} - */ - protected onDefineParameters(): void { - this.onDefineUnscopedParameters(); - - if (!this._scopingParameters.length) { - throw new Error( - 'No scoping parameters defined. At least one scoping parameter must be defined. ' + - 'Scoping parameters are defined by setting the parameterGroupName to ' + - 'ScopedCommandLineAction.ScopingParameterGroupName.' - ); - } - if (this.remainder) { - throw new Error( - 'Unscoped remainder parameters are not allowed. Remainder parameters can only be defined on ' + - 'the scoped parameter provider in onDefineScopedParameters().' - ); - } - - // Consume the remainder of the command-line, which will later be passed the scoped parser. - // This will also prevent developers from calling this.defineCommandLineRemainder(...) since - // we will have already defined it. - this.defineCommandLineRemainder({ - description: - 'Scoped parameters. Must be prefixed with "--", ex. "-- --scopedParameter ' + - 'foo --scopedFlag". For more information on available scoped parameters, use "-- --help".' - }); - } - - /** - * Retrieves the scoped CommandLineParser, which is populated after the ScopedCommandLineAction is executed. - * @internal - */ - protected _getScopedCommandLineParser(): CommandLineParser { - if (!this._scopedCommandLineParser) { - throw new Error('The scoped CommandLineParser is only populated after the action is executed.'); - } - return this._scopedCommandLineParser; - } - - /** @internal */ - protected _defineParameter(parameter: CommandLineParameter): void { - super._defineParameter(parameter); - if (parameter.groupName === ScopedCommandLineAction.ScopingParameterGroupName) { - this._scopingParameters.push(parameter); - } - } - - /** - * The child class should implement this hook to define its unscoped command-line parameters, - * e.g. by calling defineFlagParameter(). At least one scoping parameter must be defined. - * Scoping parameters are defined by setting the parameterGroupName to - * ScopedCommandLineAction.ScopingParameterGroupName. - */ - protected abstract onDefineUnscopedParameters(): void; - - /** - * The child class should implement this hook to define its scoped command-line - * parameters, e.g. by calling scopedParameterProvider.defineFlagParameter(). These - * parameters will only be available if the action is invoked with a scope. - * - * @remarks - * onDefineScopedParameters is called after the unscoped parameters have been parsed. - * The values they provide can be used to vary the defined scope parameters. - */ - protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; - - /** - * {@inheritDoc CommandLineAction.onExecute} - */ - protected abstract onExecute(): Promise; -} diff --git a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts index 82fe7ddc79d..08c104e9d90 100644 --- a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts @@ -361,34 +361,31 @@ describe(CommandLineParameter.name, () => { return commandLineParser; } - it('raises an error if env var value is not valid json', async () => { + it('raises an error if env var value is not valid json', () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[u'; - return expect( - commandLineParser.executeWithoutErrorHandling(args) - ).rejects.toThrowErrorMatchingSnapshot(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is json containing non-scalars', async () => { + it('raises an error if env var value is json containing non-scalars', () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[{}]'; - return expect( - commandLineParser.executeWithoutErrorHandling(args) - ).rejects.toThrowErrorMatchingSnapshot(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is not a valid choice', async () => { + it('raises an error if env var value is not a valid choice', () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = 'oblong'; - return expect( - commandLineParser.executeWithoutErrorHandling(args) - ).rejects.toThrowErrorMatchingSnapshot(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts index 0becb8dede7..62bd8cdc949 100644 --- a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts @@ -73,25 +73,4 @@ describe(CommandLineRemainder.name, () => { expect(copiedArgs).toMatchSnapshot(); }); - - it('parses an action input with remainder flagged options', async () => { - const commandLineParser: CommandLineParser = createParser(); - const action: CommandLineAction = commandLineParser.getAction('run'); - const args: string[] = ['run', '--title', 'The title', '--', '--the', 'remaining', '--args']; - - await commandLineParser.execute(args); - - expect(commandLineParser.selectedAction).toBe(action); - - const copiedArgs: string[] = []; - for (const parameter of action.parameters) { - copiedArgs.push(`### ${parameter.longName} output: ###`); - parameter.appendToArgList(copiedArgs); - } - - copiedArgs.push(`### remainder output: ###`); - action.remainder!.appendToArgList(copiedArgs); - - expect(copiedArgs).toMatchSnapshot(); - }); }); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts deleted file mode 100644 index 2caaaf3ffc5..00000000000 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as colors from 'colors'; - -import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; -import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; -import { CommandLineParser } from '../providers/CommandLineParser'; -import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; - -class TestScopedAction extends ScopedCommandLineAction { - public done: boolean = false; - public scopedValue: string | undefined; - private _scopeArg!: CommandLineStringParameter; - private _scopedArg!: CommandLineStringParameter; - - public constructor() { - super({ - actionName: 'scoped-action', - summary: 'does the scoped action', - documentation: 'a longer description' - }); - } - - protected async onExecute(): Promise { - expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); - this.scopedValue = this._scopedArg.value; - this.done = true; - } - - protected onDefineUnscopedParameters(): void { - this._scopeArg = this.defineStringParameter({ - parameterLongName: '--scope', - parameterGroupName: ScopedCommandLineAction.ScopingParameterGroupName, - argumentName: 'SCOPE', - description: 'The scope' - }); - } - - protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { - this._scopedArg = scopedParameterProvider.defineStringParameter({ - parameterLongName: `--scoped-${this._scopeArg.value}`, - argumentName: 'SCOPED', - description: 'The scoped argument.' - }); - } -} - -class TestCommandLine extends CommandLineParser { - public constructor() { - super({ - toolFilename: 'example', - toolDescription: 'An example project' - }); - - this.addAction(new TestScopedAction()); - } - - protected onDefineParameters(): void { - // no parameters - } -} - -describe(CommandLineParser.name, () => { - it('throws on unknown scoped arg', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; - - return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); - }); - - it('throws on missing positional arg divider with unknown positional args', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const args: string[] = ['scoped-action', '--scope', 'foo', 'bar']; - - return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); - }); - - it('executes a scoped action', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - - await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - - expect(commandLineParser.selectedAction).toBeDefined(); - expect(commandLineParser.selectedAction!.actionName).toEqual('scoped-action'); - - const action: TestScopedAction = commandLineParser.selectedAction as TestScopedAction; - expect(action.done).toBe(true); - expect(action.scopedValue).toBe('bar'); - }); - - it('prints the action help', () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const helpText: string = colors.stripColors( - commandLineParser.getAction('scoped-action').renderHelpText() - ); - expect(helpText).toMatchSnapshot(); - }); - - it('prints the scoped action help', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - const unscopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = - commandLineParser.getAction('scoped-action') as TestScopedAction & { - _getScopedCommandLineParser(): CommandLineParser; - }; - const scopedCommandLineParser: CommandLineParser = unscopedAction._getScopedCommandLineParser(); - const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); - expect(helpText).toMatchSnapshot(); - }); -}); diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap index 1250aa3605e..e11dc7a569d 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap @@ -12,19 +12,6 @@ Array [ ] `; -exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` -Array [ - "### --title output: ###", - "--title", - "The title", - "### remainder output: ###", - "--", - "--the", - "remaining", - "--args", -] -`; - exports[`CommandLineRemainder prints the action help 1`] = ` "usage: example run [-h] [--title TEXT] ... diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap deleted file mode 100644 index 3179eb4c50d..00000000000 --- a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CommandLineParser prints the action help 1`] = ` -"usage: example scoped-action [-h] [--scope SCOPE] ... - -a longer description - -Positional arguments: - \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- - --scopedParameter foo --scopedFlag\\". For more information on - available scoped parameters, use \\"-- --help\\". - -Optional arguments: - -h, --help Show this help message and exit. - -Optional scoping arguments: - --scope SCOPE The scope -" -`; - -exports[`CommandLineParser prints the scoped action help 1`] = ` -"usage: example scoped-action --scope foo -- [-h] [--scoped-foo SCOPED] - -a longer description - -Optional arguments: - -h, --help Show this help message and exit. - --scoped-foo SCOPED The scoped argument. - -For more information on available unscoped parameters, use \\"example -scoped-action --help\\" -" -`; - -exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; - -exports[`CommandLineParser throws on unknown scoped arg 1`] = ` -"example scoped-action --scope foo --: error: Unrecognized arguments: --scoped-bar baz. -" -`; From b20cb5ced265b1ca855596cfdd3ccc11e735b2ca Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Wed, 20 Apr 2022 14:37:34 -0700 Subject: [PATCH 05/73] Update argparse version Add new remainder test Add ScopedCommandLineParser Revert "Update argparse version" This reverts commit 6f917d351eede7297e65132dbb3f33a3622be6c3. Cleanup More cleanup Rush change Updated docstring Tweaks to allow for unscoped calls to scoping actions, allowing the developer to decide if the scope is required Apply suggestions from code review Co-authored-by: Ian Clanton-Thuon Call the onExecute directly Use a symbol for the 'scoping' parameter group Break a circular reference. --- ...de-ScopedCommandLine_2022-04-21-22-00.json | 10 + common/reviews/api/ts-command-line.api.md | 29 ++- libraries/ts-command-line/src/Constants.ts | 2 + libraries/ts-command-line/src/index.ts | 5 +- .../src/parameters/BaseClasses.ts | 5 + .../src/parameters/CommandLineDefinition.ts | 7 + .../src/providers/CommandLineAction.ts | 6 +- .../providers/CommandLineParameterProvider.ts | 92 ++++--- .../src/providers/CommandLineParser.ts | 17 +- .../src/providers/ScopedCommandLineAction.ts | 229 ++++++++++++++++++ .../src/test/ActionlessParser.test.ts | 18 ++ .../src/test/CommandLineParameter.test.ts | 21 +- .../src/test/CommandLineRemainder.test.ts | 21 ++ .../src/test/ScopedCommandLineAction.test.ts | 154 ++++++++++++ .../CommandLineRemainder.test.ts.snap | 13 + .../ScopedCommandLineAction.test.ts.snap | 64 +++++ 16 files changed, 645 insertions(+), 48 deletions(-) create mode 100644 common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json create mode 100644 libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts create mode 100644 libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts create mode 100644 libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap diff --git a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json new file mode 100644 index 00000000000..7f0cc08de2a --- /dev/null +++ b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/ts-command-line", + "comment": "Add ScopedCommandLineAction class, which allows for the definition of actions that have dynamic arguments whose definition depends on a provided scope. See https://github.com/microsoft/rushstack/pull/3364", + "type": "minor" + } + ], + "packageName": "@rushstack/ts-command-line" +} \ No newline at end of file diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index aea2b69eec2..c9964e7a8ab 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -20,7 +20,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { protected abstract onDefineParameters(): void; protected abstract onExecute(): Promise; // @internal - _processParsedData(data: _ICommandLineParserData): void; + _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; readonly summary: string; } @@ -115,6 +115,7 @@ export abstract class CommandLineParameter { _getSupplementaryNotes(supplementaryNotes: string[]): void; abstract get kind(): CommandLineParameterKind; readonly longName: string; + readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; // @internal _parserKey: string | undefined; protected reportInvalidData(data: any): never; @@ -148,6 +149,8 @@ export abstract class CommandLineParameterProvider { defineFlagParameter(definition: ICommandLineFlagDefinition): CommandLineFlagParameter; defineIntegerListParameter(definition: ICommandLineIntegerListDefinition): CommandLineIntegerListParameter; defineIntegerParameter(definition: ICommandLineIntegerDefinition): CommandLineIntegerParameter; + // @internal (undocumented) + protected _defineParameter(parameter: CommandLineParameter): void; defineStringListParameter(definition: ICommandLineStringListDefinition): CommandLineStringListParameter; defineStringParameter(definition: ICommandLineStringDefinition): CommandLineStringParameter; // @internal @@ -164,9 +167,10 @@ export abstract class CommandLineParameterProvider { get parameters(): ReadonlyArray; get parametersProcessed(): boolean; // @internal (undocumented) - protected _processParsedData(data: _ICommandLineParserData): void; + protected _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; get remainder(): CommandLineRemainder | undefined; renderHelpText(): string; + renderUsageText(): string; } // @public @@ -249,6 +253,7 @@ export class DynamicCommandLineParser extends CommandLineParser { export interface IBaseCommandLineDefinition { description: string; environmentVariable?: string; + parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; parameterLongName: string; parameterShortName?: string; required?: boolean; @@ -306,6 +311,7 @@ export interface _ICommandLineParserData { export interface ICommandLineParserOptions { enableTabCompletionAction?: boolean; toolDescription: string; + toolEpilog?: string; toolFilename: string; } @@ -323,4 +329,23 @@ export interface ICommandLineStringDefinition extends IBaseCommandLineDefinition export interface ICommandLineStringListDefinition extends IBaseCommandLineDefinitionWithArgument { } +// @public +export abstract class ScopedCommandLineAction extends CommandLineAction { + constructor(options: ICommandLineActionOptions); + // @internal (undocumented) + protected _defineParameter(parameter: CommandLineParameter): void; + // @internal + _execute(): Promise; + // @internal + protected _getScopedCommandLineParser(): CommandLineParser; + protected onDefineParameters(): void; + protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; + protected abstract onDefineUnscopedParameters(): void; + protected abstract onExecute(): Promise; + get parameters(): ReadonlyArray; + // @internal + _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP; +} + ``` diff --git a/libraries/ts-command-line/src/Constants.ts b/libraries/ts-command-line/src/Constants.ts index 7a5a285280e..5b2d48b00c8 100644 --- a/libraries/ts-command-line/src/Constants.ts +++ b/libraries/ts-command-line/src/Constants.ts @@ -12,3 +12,5 @@ export const enum CommandLineConstants { */ TabCompletionActionName = 'tab-complete' } + +export const SCOPING_PARAMETER_GROUP: unique symbol = Symbol('scoping'); diff --git a/libraries/ts-command-line/src/index.ts b/libraries/ts-command-line/src/index.ts index 2c82b011bc0..1cd47635a41 100644 --- a/libraries/ts-command-line/src/index.ts +++ b/libraries/ts-command-line/src/index.ts @@ -8,6 +8,8 @@ */ export { CommandLineAction, ICommandLineActionOptions } from './providers/CommandLineAction'; +export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; +export { ScopedCommandLineAction } from './providers/ScopedCommandLineAction'; export { IBaseCommandLineDefinition, @@ -43,9 +45,6 @@ export { } from './providers/CommandLineParameterProvider'; export { ICommandLineParserOptions, CommandLineParser } from './providers/CommandLineParser'; - -export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; - export { DynamicCommandLineParser } from './providers/DynamicCommandLineParser'; export { CommandLineConstants } from './Constants'; diff --git a/libraries/ts-command-line/src/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 8b61cb49a08..1950696fae6 100644 --- a/libraries/ts-command-line/src/parameters/BaseClasses.ts +++ b/libraries/ts-command-line/src/parameters/BaseClasses.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import type { SCOPING_PARAMETER_GROUP } from '../Constants'; import { IBaseCommandLineDefinition, IBaseCommandLineDefinitionWithArgument } from './CommandLineDefinition'; /** @@ -53,6 +54,9 @@ export abstract class CommandLineParameter { /** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */ public readonly shortName: string | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.parameterGroup} */ + public readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.description} */ public readonly description: string; @@ -69,6 +73,7 @@ export abstract class CommandLineParameter { public constructor(definition: IBaseCommandLineDefinition) { this.longName = definition.parameterLongName; this.shortName = definition.parameterShortName; + this.parameterGroup = definition.parameterGroup; this.description = definition.description; this.required = !!definition.required; this.environmentVariable = definition.environmentVariable; diff --git a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts index 3d7913efb2c..a5322d02708 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import type { SCOPING_PARAMETER_GROUP } from '../Constants'; + /** * For use with CommandLineParser, this interface represents a generic command-line parameter * @@ -17,6 +19,11 @@ export interface IBaseCommandLineDefinition { */ parameterShortName?: string; + /** + * An optional parameter group name, shown when invoking the tool with "--help" + */ + parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; + /** * Documentation for the parameter that will be shown when invoking the tool with "--help" */ diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index a0bd8deb4d9..e9f5fb874ea 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -2,7 +2,9 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; + import { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; +import type { ICommandLineParserOptions } from './CommandLineParser'; /** * Options for the CommandLineAction constructor. @@ -90,8 +92,8 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * This is called internally by CommandLineParser.execute() * @internal */ - public _processParsedData(data: ICommandLineParserData): void { - super._processParsedData(data); + public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + super._processParsedData(parserOptions, data); } /** diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 48e0818d01f..7e02763190b 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -2,7 +2,8 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; -import { + +import type { ICommandLineChoiceDefinition, ICommandLineChoiceListDefinition, ICommandLineIntegerDefinition, @@ -12,6 +13,7 @@ import { ICommandLineStringListDefinition, ICommandLineRemainderDefinition } from '../parameters/CommandLineDefinition'; +import type { ICommandLineParserOptions } from './CommandLineParser'; import { CommandLineParameter, CommandLineParameterWithArgument, @@ -25,6 +27,7 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; import { CommandLineStringListParameter } from '../parameters/CommandLineStringListParameter'; import { CommandLineRemainder } from '../parameters/CommandLineRemainder'; +import { SCOPING_PARAMETER_GROUP } from '../Constants'; /** * This is the argparse result data object @@ -46,6 +49,7 @@ export abstract class CommandLineParameterProvider { private _parameters: CommandLineParameter[]; private _parametersByLongName: Map; + private _parameterGroupsByName: Map; private _parametersProcessed: boolean; private _remainder: CommandLineRemainder | undefined; @@ -53,7 +57,8 @@ export abstract class CommandLineParameterProvider { // Third party code should not inherit subclasses or call this constructor public constructor() { this._parameters = []; - this._parametersByLongName = new Map(); + this._parametersByLongName = new Map(); + this._parameterGroupsByName = new Map(); this._parametersProcessed = false; } @@ -207,6 +212,7 @@ export abstract class CommandLineParameterProvider { public getIntegerListParameter(parameterLongName: string): CommandLineIntegerListParameter { return this._getParameter(parameterLongName, CommandLineParameterKind.IntegerList); } + /** * Defines a command-line parameter whose argument is a single text string. * @@ -297,6 +303,13 @@ export abstract class CommandLineParameterProvider { return this._getArgumentParser().formatHelp(); } + /** + * Generates the command-line usage text. + */ + public renderUsageText(): string { + return this._getArgumentParser().formatUsage(); + } + /** * Returns a object which maps the long name of each parameter in this.parameters * to the stringified form of its value. This is useful for logging telemetry, but @@ -349,7 +362,7 @@ export abstract class CommandLineParameterProvider { protected abstract _getArgumentParser(): argparse.ArgumentParser; /** @internal */ - protected _processParsedData(data: ICommandLineParserData): void { + protected _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { if (this._parametersProcessed) { throw new Error('Command Line Parser Data was already processed'); } @@ -367,28 +380,8 @@ export abstract class CommandLineParameterProvider { this._parametersProcessed = true; } - private _generateKey(): string { - return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); - } - - private _getParameter( - parameterLongName: string, - expectedKind: CommandLineParameterKind - ): T { - const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); - if (!parameter) { - throw new Error(`The parameter "${parameterLongName}" is not defined`); - } - if (parameter.kind !== expectedKind) { - throw new Error( - `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + - ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` - ); - } - return parameter as T; - } - - private _defineParameter(parameter: CommandLineParameter): void { + /** @internal */ + protected _defineParameter(parameter: CommandLineParameter): void { if (this._remainder) { throw new Error( 'defineCommandLineRemainder() was already called for this provider;' + @@ -455,10 +448,32 @@ export abstract class CommandLineParameterProvider { break; } - const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); - argumentParser.addArgument(names, { ...argparseOptions }); + let argumentGroup: argparse.ArgumentGroup | undefined; + if (parameter.parameterGroup) { + argumentGroup = this._parameterGroupsByName.get(parameter.parameterGroup); + if (!argumentGroup) { + let parameterGroupName: string; + if (typeof parameter.parameterGroup === 'string') { + parameterGroupName = parameter.parameterGroup; + } else if (parameter.parameterGroup === SCOPING_PARAMETER_GROUP) { + parameterGroupName = 'scoping'; + } else { + throw new Error('Unexpected parameter group: ' + parameter.parameterGroup); + } + + argumentGroup = this._getArgumentParser().addArgumentGroup({ + title: `Optional ${parameterGroupName} arguments` + }); + this._parameterGroupsByName.set(parameter.parameterGroup, argumentGroup); + } + } else { + argumentGroup = this._getArgumentParser(); + } + + argumentGroup.addArgument(names, { ...argparseOptions }); + if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { - argumentParser.addArgument(parameter.undocumentedSynonyms, { + argumentGroup.addArgument(parameter.undocumentedSynonyms, { ...argparseOptions, help: argparse.Const.SUPPRESS }); @@ -467,4 +482,25 @@ export abstract class CommandLineParameterProvider { this._parameters.push(parameter); this._parametersByLongName.set(parameter.longName, parameter); } + + private _generateKey(): string { + return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); + } + + private _getParameter( + parameterLongName: string, + expectedKind: CommandLineParameterKind + ): T { + const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); + if (!parameter) { + throw new Error(`The parameter "${parameterLongName}" is not defined`); + } + if (parameter.kind !== expectedKind) { + throw new Error( + `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + + ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` + ); + } + return parameter as T; + } } diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index f14888886ec..25fc95342b1 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -24,6 +24,12 @@ export interface ICommandLineParserOptions { */ toolDescription: string; + /** + * An optional string to append at the end of the "--help" main page. If not provided, an epilog + * will be automatically generated based on the toolFilename. + */ + toolEpilog?: string; + /** * Set to true to auto-define a tab completion action. False by default. */ @@ -68,7 +74,8 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( - `For detailed help about a specific command, use: ${this._options.toolFilename} -h` + this._options.toolEpilog ?? + `For detailed help about a specific command, use: ${this._options.toolFilename} -h` ) }); @@ -194,19 +201,21 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { // 0=node.exe, 1=script name args = process.argv.slice(2); } - if (args.length === 0) { + if (this.actions.length > 0 && 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; } const data: ICommandLineParserData = this._argumentParser.parseArgs(args); - this._processParsedData(data); + this._processParsedData(this._options, data); for (const action of this._actions) { if (action.actionName === data.action) { this.selectedAction = action; - action._processParsedData(data); + action._processParsedData(this._options, data); break; } } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts new file mode 100644 index 00000000000..bdc10a516f8 --- /dev/null +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { CommandLineAction, ICommandLineActionOptions } from './CommandLineAction'; +import { CommandLineParser, ICommandLineParserOptions } from './CommandLineParser'; +import { CommandLineParserExitError } from './CommandLineParserExitError'; +import type { CommandLineParameter } from '../parameters/BaseClasses'; +import type { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; +import { SCOPING_PARAMETER_GROUP } from '../Constants'; + +interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { + readonly actionOptions: ICommandLineActionOptions; + readonly unscopedActionParameters: ReadonlyArray; + readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; +} + +/** + * A CommandLineParser used exclusively to parse the scoped command-line parameters + * for a ScopedCommandLineAction. + */ +class InternalScopedCommandLineParser extends CommandLineParser { + private _internalOptions: IInternalScopedCommandLineParserOptions; + + public constructor(options: IInternalScopedCommandLineParserOptions) { + // We can run the parser directly because we are not going to use it for any other actions, + // so construct a special options object to make the "--help" text more useful. + const scopingArgs: string[] = []; + for (const parameter of options.unscopedActionParameters) { + parameter.appendToArgList(scopingArgs); + } + const unscopedToolName: string = `${options.toolFilename} ${options.actionOptions.actionName}`; + const scopedCommandLineParserOptions: ICommandLineParserOptions = { + toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, + toolDescription: options.actionOptions.documentation, + toolEpilog: `For more information on available unscoped parameters, use "${unscopedToolName} --help"`, + enableTabCompletionAction: false + }; + + super(scopedCommandLineParserOptions); + this._internalOptions = options; + this._internalOptions.onDefineScopedParameters(this); + } + + protected onDefineParameters(): void { + // No-op. Parameters are manually defined in the constructor. + } +} + +/** + * Represents a sub-command that is part of the CommandLineParser command-line. + * Applications should create subclasses of ScopedCommandLineAction corresponding to + * each action that they want to expose. + * + * The action name should be comprised of lower case words separated by hyphens + * or colons. The name should include an English verb (e.g. "deploy"). Use a + * hyphen to separate words (e.g. "upload-docs"). A group of related commands + * can be prefixed with a colon (e.g. "docs:generate", "docs:deploy", + * "docs:serve", etc). + * + * Scoped commands allow for different parameters to be specified for different + * provided scoping values. For example, the "scoped-action --scope A" command + * may allow for different scoped arguments to be specified than the "scoped-action + * --scope B" command. + * + * Scoped arguments are specified after the "--" pseudo-argument. For example, + * "scoped-action --scope A -- --scopedFoo --scopedBar". + * + * @public + */ +export abstract class ScopedCommandLineAction extends CommandLineAction { + private _options: ICommandLineActionOptions; + private _scopingParameters: CommandLineParameter[]; + private _unscopedParserOptions: ICommandLineParserOptions | undefined; + private _scopedCommandLineParser: InternalScopedCommandLineParser | undefined; + + /** + * The required group name to apply to all scoping parameters. At least one parameter + * must be defined with this group name. + */ + public static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP = SCOPING_PARAMETER_GROUP; + + public constructor(options: ICommandLineActionOptions) { + super(options); + + this._options = options; + this._scopingParameters = []; + } + + /** + * {@inheritDoc CommandLineParameterProvider.parameters} + */ + public get parameters(): ReadonlyArray { + if (this._scopedCommandLineParser) { + return [...super.parameters, ...this._scopedCommandLineParser.parameters]; + } else { + return super.parameters; + } + } + + /** + * {@inheritdoc CommandLineAction._processParsedData} + * @internal + */ + public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + // override + super._processParsedData(parserOptions, data); + + this._unscopedParserOptions = parserOptions; + + // Generate the scoped parser using the parent parser information. We can only create this after we + // have parsed the data, since the parameter values are used during construction. + this._scopedCommandLineParser = new InternalScopedCommandLineParser({ + ...parserOptions, + actionOptions: this._options, + unscopedActionParameters: this.parameters, + onDefineScopedParameters: this.onDefineScopedParameters.bind(this) + }); + } + + /** + * {@inheritdoc CommandLineAction._execute} + * @internal + */ + public async _execute(): Promise { + // override + if (!this._unscopedParserOptions || !this._scopedCommandLineParser) { + throw new Error('The CommandLineAction parameters must be processed before execution.'); + } + if (!this.remainder) { + throw new Error('CommandLineAction.onDefineParameters must be called before execution.'); + } + + // The '--' argument is required to separate the action parameters from the scoped parameters, + // so it needs to be trimmed. If remainder values are provided but no '--' is found, then throw. + const scopedArgs: string[] = []; + if (this.remainder.values.length) { + if (this.remainder.values[0] !== '--') { + // Immitate argparse behavior and log out usage text before throwing. + console.log(this.renderUsageText()); + throw new CommandLineParserExitError( + // argparse sets exit code 2 for invalid arguments + 2, + // model the message off of the built-in "unrecognized arguments" message + `${this._unscopedParserOptions.toolFilename} ${this.actionName}: error: Unrecognized ` + + `arguments: ${this.remainder.values[0]}.` + ); + } + scopedArgs.push(...this.remainder.values.slice(1)); + } + + // Call the scoped parser using only the scoped args. + await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + await super._execute(); + return; + } + + /** + * {@inheritdoc CommandLineParameterProvider.onDefineParameters} + */ + protected onDefineParameters(): void { + this.onDefineUnscopedParameters(); + + if (!this._scopingParameters.length) { + throw new Error( + 'No scoping parameters defined. At least one scoping parameter must be defined. ' + + 'Scoping parameters are defined by setting the parameterGroupName to ' + + 'ScopedCommandLineAction.ScopingParameterGroupName.' + ); + } + if (this.remainder) { + throw new Error( + 'Unscoped remainder parameters are not allowed. Remainder parameters can only be defined on ' + + 'the scoped parameter provider in onDefineScopedParameters().' + ); + } + + // Consume the remainder of the command-line, which will later be passed the scoped parser. + // This will also prevent developers from calling this.defineCommandLineRemainder(...) since + // we will have already defined it. + this.defineCommandLineRemainder({ + description: + 'Scoped parameters. Must be prefixed with "--", ex. "-- --scopedParameter ' + + 'foo --scopedFlag". For more information on available scoped parameters, use "-- --help".' + }); + } + + /** + * Retrieves the scoped CommandLineParser, which is populated after the ScopedCommandLineAction is executed. + * @internal + */ + protected _getScopedCommandLineParser(): CommandLineParser { + if (!this._scopedCommandLineParser) { + throw new Error('The scoped CommandLineParser is only populated after the action is executed.'); + } + return this._scopedCommandLineParser; + } + + /** @internal */ + protected _defineParameter(parameter: CommandLineParameter): void { + super._defineParameter(parameter); + if (parameter.parameterGroup === ScopedCommandLineAction.ScopingParameterGroup) { + this._scopingParameters.push(parameter); + } + } + + /** + * The child class should implement this hook to define its unscoped command-line parameters, + * e.g. by calling defineFlagParameter(). At least one scoping parameter must be defined. + * Scoping parameters are defined by setting the parameterGroupName to + * ScopedCommandLineAction.ScopingParameterGroupName. + */ + protected abstract onDefineUnscopedParameters(): void; + + /** + * The child class should implement this hook to define its scoped command-line + * parameters, e.g. by calling scopedParameterProvider.defineFlagParameter(). These + * parameters will only be available if the action is invoked with a scope. + * + * @remarks + * onDefineScopedParameters is called after the unscoped parameters have been parsed. + * The values they provide can be used to vary the defined scope parameters. + */ + protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; + + /** + * {@inheritDoc CommandLineAction.onExecute} + */ + protected abstract onExecute(): Promise; +} diff --git a/libraries/ts-command-line/src/test/ActionlessParser.test.ts b/libraries/ts-command-line/src/test/ActionlessParser.test.ts index dea5cdc0a64..71c0e7edd16 100644 --- a/libraries/ts-command-line/src/test/ActionlessParser.test.ts +++ b/libraries/ts-command-line/src/test/ActionlessParser.test.ts @@ -6,6 +6,7 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter class TestCommandLine extends CommandLineParser { public flag!: CommandLineFlagParameter; + public done: boolean = false; public constructor() { super({ @@ -14,6 +15,11 @@ class TestCommandLine extends CommandLineParser { }); } + protected async onExecute(): Promise { + await super.onExecute(); + this.done = true; + } + protected onDefineParameters(): void { this.flag = this.defineFlagParameter({ parameterLongName: '--flag', @@ -23,11 +29,22 @@ class TestCommandLine extends CommandLineParser { } describe(`Actionless ${CommandLineParser.name}`, () => { + it('parses an empty arg list', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + + await commandLineParser.execute([]); + + expect(commandLineParser.done).toBe(true); + expect(commandLineParser.selectedAction).toBeUndefined(); + expect(commandLineParser.flag.value).toBe(false); + }); + it('parses a flag', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); await commandLineParser.execute(['--flag']); + expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); }); @@ -41,6 +58,7 @@ describe(`Actionless ${CommandLineParser.name}`, () => { await commandLineParser.execute(['--flag', 'the', 'remaining', 'args']); + expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); expect(commandLineParser.remainder!.values).toEqual(['the', 'remaining', 'args']); diff --git a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts index 08c104e9d90..af389928f9a 100644 --- a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts @@ -361,31 +361,34 @@ describe(CommandLineParameter.name, () => { return commandLineParser; } - it('raises an error if env var value is not valid json', () => { + it('raises an error if env var value is not valid json', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[u'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is json containing non-scalars', () => { + it('raises an error if env var value is json containing non-scalars', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[{}]'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is not a valid choice', () => { + it('raises an error if env var value is not a valid choice', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = 'oblong'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts index 62bd8cdc949..0becb8dede7 100644 --- a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts @@ -73,4 +73,25 @@ describe(CommandLineRemainder.name, () => { expect(copiedArgs).toMatchSnapshot(); }); + + it('parses an action input with remainder flagged options', async () => { + const commandLineParser: CommandLineParser = createParser(); + const action: CommandLineAction = commandLineParser.getAction('run'); + const args: string[] = ['run', '--title', 'The title', '--', '--the', 'remaining', '--args']; + + await commandLineParser.execute(args); + + expect(commandLineParser.selectedAction).toBe(action); + + const copiedArgs: string[] = []; + for (const parameter of action.parameters) { + copiedArgs.push(`### ${parameter.longName} output: ###`); + parameter.appendToArgList(copiedArgs); + } + + copiedArgs.push(`### remainder output: ###`); + action.remainder!.appendToArgList(copiedArgs); + + expect(copiedArgs).toMatchSnapshot(); + }); }); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts new file mode 100644 index 00000000000..05336946ef4 --- /dev/null +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as colors from 'colors'; + +import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; +import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; +import { CommandLineParser } from '../providers/CommandLineParser'; +import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; +import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter'; + +class TestScopedAction extends ScopedCommandLineAction { + public done: boolean = false; + public scopedValue: string | undefined; + private _verboseArg!: CommandLineFlagParameter; + private _scopeArg!: CommandLineStringParameter; + private _scopedArg: CommandLineStringParameter | undefined; + + public constructor() { + super({ + actionName: 'scoped-action', + summary: 'does the scoped action', + documentation: 'a longer description' + }); + } + + protected async onExecute(): Promise { + if (this._scopedArg) { + expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); + this.scopedValue = this._scopedArg.value; + } + this.done = true; + } + + protected onDefineUnscopedParameters(): void { + this._verboseArg = this.defineFlagParameter({ + parameterLongName: '--verbose', + description: 'A flag parameter.' + }); + + this._scopeArg = this.defineStringParameter({ + parameterLongName: '--scope', + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup, + argumentName: 'SCOPE', + description: 'The scope' + }); + } + + protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { + if (this._scopeArg.value) { + this._scopedArg = scopedParameterProvider.defineStringParameter({ + parameterLongName: `--scoped-${this._scopeArg.value}`, + argumentName: 'SCOPED', + description: 'The scoped argument.' + }); + } + } +} + +class TestCommandLine extends CommandLineParser { + public constructor() { + super({ + toolFilename: 'example', + toolDescription: 'An example project' + }); + + this.addAction(new TestScopedAction()); + } + + protected onDefineParameters(): void { + // no parameters + } +} + +describe(CommandLineParser.name, () => { + it('throws on unknown scoped arg', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; + + await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('throws on missing positional arg divider with unknown positional args', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', 'bar']; + + await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('executes a scoped action', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + + expect(commandLineParser.selectedAction).toBeDefined(); + expect(commandLineParser.selectedAction!.actionName).toEqual('scoped-action'); + + const action: TestScopedAction = commandLineParser.selectedAction as TestScopedAction; + expect(action.done).toBe(true); + expect(action.scopedValue).toBe('bar'); + }); + + it('prints the action help', () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const helpText: string = colors.stripColors( + commandLineParser.getAction('scoped-action').renderHelpText() + ); + expect(helpText).toMatchSnapshot(); + }); + + it('prints the scoped action help', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action to populate the help text. + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + const scopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = + commandLineParser.getAction('scoped-action') as TestScopedAction & { + _getScopedCommandLineParser(): CommandLineParser; + }; + const scopedCommandLineParser: CommandLineParser = scopedAction._getScopedCommandLineParser(); + const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); + expect(helpText).toMatchSnapshot(); + }); + + it('prints the unscoped action parameter map', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--verbose']); + const scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(2); + const parameterStringMap: Record = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + }); + + it('prints the scoped action parameter map', async () => { + let commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo']); + let scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(3); + let parameterStringMap: Record = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + + commandLineParser = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + scopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(3); + parameterStringMap = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + }); +}); diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap index e11dc7a569d..1250aa3605e 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap @@ -12,6 +12,19 @@ Array [ ] `; +exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` +Array [ + "### --title output: ###", + "--title", + "The title", + "### remainder output: ###", + "--", + "--the", + "remaining", + "--args", +] +`; + exports[`CommandLineRemainder prints the action help 1`] = ` "usage: example run [-h] [--title TEXT] ... diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap new file mode 100644 index 00000000000..ca696e05d3d --- /dev/null +++ b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommandLineParser prints the action help 1`] = ` +"usage: example scoped-action [-h] [--verbose] [--scope SCOPE] ... + +a longer description + +Positional arguments: + \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- + --scopedParameter foo --scopedFlag\\". For more information on + available scoped parameters, use \\"-- --help\\". + +Optional arguments: + -h, --help Show this help message and exit. + --verbose A flag parameter. + +Optional scoping arguments: + --scope SCOPE The scope +" +`; + +exports[`CommandLineParser prints the scoped action help 1`] = ` +"usage: example scoped-action --scope foo -- [-h] [--scoped-foo SCOPED] + +a longer description + +Optional arguments: + -h, --help Show this help message and exit. + --scoped-foo SCOPED The scoped argument. + +For more information on available unscoped parameters, use \\"example +scoped-action --help\\" +" +`; + +exports[`CommandLineParser prints the scoped action parameter map 1`] = ` +Object { + "--scope": "\\"foo\\"", + "--scoped-foo": undefined, + "--verbose": "false", +} +`; + +exports[`CommandLineParser prints the scoped action parameter map 2`] = ` +Object { + "--scope": "\\"foo\\"", + "--scoped-foo": "\\"bar\\"", + "--verbose": "false", +} +`; + +exports[`CommandLineParser prints the unscoped action parameter map 1`] = ` +Object { + "--scope": undefined, + "--verbose": "true", +} +`; + +exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; + +exports[`CommandLineParser throws on unknown scoped arg 1`] = ` +"example scoped-action --scope foo --: error: Unrecognized arguments: --scoped-bar baz. +" +`; From ba2d5eb6e2a15f5f736e49d3a5cd95963bf2354e Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 20:02:03 -0700 Subject: [PATCH 06/73] Revert "Update argparse version" This reverts commit b20cb5ced265b1ca855596cfdd3ccc11e735b2ca. --- ...de-ScopedCommandLine_2022-04-21-22-00.json | 10 - common/reviews/api/ts-command-line.api.md | 29 +-- libraries/ts-command-line/src/Constants.ts | 2 - libraries/ts-command-line/src/index.ts | 5 +- .../src/parameters/BaseClasses.ts | 5 - .../src/parameters/CommandLineDefinition.ts | 7 - .../src/providers/CommandLineAction.ts | 6 +- .../providers/CommandLineParameterProvider.ts | 92 +++---- .../src/providers/CommandLineParser.ts | 17 +- .../src/providers/ScopedCommandLineAction.ts | 229 ------------------ .../src/test/ActionlessParser.test.ts | 18 -- .../src/test/CommandLineParameter.test.ts | 21 +- .../src/test/CommandLineRemainder.test.ts | 21 -- .../src/test/ScopedCommandLineAction.test.ts | 154 ------------ .../CommandLineRemainder.test.ts.snap | 13 - .../ScopedCommandLineAction.test.ts.snap | 64 ----- 16 files changed, 48 insertions(+), 645 deletions(-) delete mode 100644 common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json delete mode 100644 libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts delete mode 100644 libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts delete mode 100644 libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap diff --git a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json deleted file mode 100644 index 7f0cc08de2a..00000000000 --- a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@rushstack/ts-command-line", - "comment": "Add ScopedCommandLineAction class, which allows for the definition of actions that have dynamic arguments whose definition depends on a provided scope. See https://github.com/microsoft/rushstack/pull/3364", - "type": "minor" - } - ], - "packageName": "@rushstack/ts-command-line" -} \ No newline at end of file diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index c9964e7a8ab..aea2b69eec2 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -20,7 +20,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { protected abstract onDefineParameters(): void; protected abstract onExecute(): Promise; // @internal - _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + _processParsedData(data: _ICommandLineParserData): void; readonly summary: string; } @@ -115,7 +115,6 @@ export abstract class CommandLineParameter { _getSupplementaryNotes(supplementaryNotes: string[]): void; abstract get kind(): CommandLineParameterKind; readonly longName: string; - readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; // @internal _parserKey: string | undefined; protected reportInvalidData(data: any): never; @@ -149,8 +148,6 @@ export abstract class CommandLineParameterProvider { defineFlagParameter(definition: ICommandLineFlagDefinition): CommandLineFlagParameter; defineIntegerListParameter(definition: ICommandLineIntegerListDefinition): CommandLineIntegerListParameter; defineIntegerParameter(definition: ICommandLineIntegerDefinition): CommandLineIntegerParameter; - // @internal (undocumented) - protected _defineParameter(parameter: CommandLineParameter): void; defineStringListParameter(definition: ICommandLineStringListDefinition): CommandLineStringListParameter; defineStringParameter(definition: ICommandLineStringDefinition): CommandLineStringParameter; // @internal @@ -167,10 +164,9 @@ export abstract class CommandLineParameterProvider { get parameters(): ReadonlyArray; get parametersProcessed(): boolean; // @internal (undocumented) - protected _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + protected _processParsedData(data: _ICommandLineParserData): void; get remainder(): CommandLineRemainder | undefined; renderHelpText(): string; - renderUsageText(): string; } // @public @@ -253,7 +249,6 @@ export class DynamicCommandLineParser extends CommandLineParser { export interface IBaseCommandLineDefinition { description: string; environmentVariable?: string; - parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; parameterLongName: string; parameterShortName?: string; required?: boolean; @@ -311,7 +306,6 @@ export interface _ICommandLineParserData { export interface ICommandLineParserOptions { enableTabCompletionAction?: boolean; toolDescription: string; - toolEpilog?: string; toolFilename: string; } @@ -329,23 +323,4 @@ export interface ICommandLineStringDefinition extends IBaseCommandLineDefinition export interface ICommandLineStringListDefinition extends IBaseCommandLineDefinitionWithArgument { } -// @public -export abstract class ScopedCommandLineAction extends CommandLineAction { - constructor(options: ICommandLineActionOptions); - // @internal (undocumented) - protected _defineParameter(parameter: CommandLineParameter): void; - // @internal - _execute(): Promise; - // @internal - protected _getScopedCommandLineParser(): CommandLineParser; - protected onDefineParameters(): void; - protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; - protected abstract onDefineUnscopedParameters(): void; - protected abstract onExecute(): Promise; - get parameters(): ReadonlyArray; - // @internal - _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; - static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP; -} - ``` diff --git a/libraries/ts-command-line/src/Constants.ts b/libraries/ts-command-line/src/Constants.ts index 5b2d48b00c8..7a5a285280e 100644 --- a/libraries/ts-command-line/src/Constants.ts +++ b/libraries/ts-command-line/src/Constants.ts @@ -12,5 +12,3 @@ export const enum CommandLineConstants { */ TabCompletionActionName = 'tab-complete' } - -export const SCOPING_PARAMETER_GROUP: unique symbol = Symbol('scoping'); diff --git a/libraries/ts-command-line/src/index.ts b/libraries/ts-command-line/src/index.ts index 1cd47635a41..2c82b011bc0 100644 --- a/libraries/ts-command-line/src/index.ts +++ b/libraries/ts-command-line/src/index.ts @@ -8,8 +8,6 @@ */ export { CommandLineAction, ICommandLineActionOptions } from './providers/CommandLineAction'; -export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; -export { ScopedCommandLineAction } from './providers/ScopedCommandLineAction'; export { IBaseCommandLineDefinition, @@ -45,6 +43,9 @@ export { } from './providers/CommandLineParameterProvider'; export { ICommandLineParserOptions, CommandLineParser } from './providers/CommandLineParser'; + +export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; + export { DynamicCommandLineParser } from './providers/DynamicCommandLineParser'; export { CommandLineConstants } from './Constants'; diff --git a/libraries/ts-command-line/src/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 1950696fae6..8b61cb49a08 100644 --- a/libraries/ts-command-line/src/parameters/BaseClasses.ts +++ b/libraries/ts-command-line/src/parameters/BaseClasses.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { SCOPING_PARAMETER_GROUP } from '../Constants'; import { IBaseCommandLineDefinition, IBaseCommandLineDefinitionWithArgument } from './CommandLineDefinition'; /** @@ -54,9 +53,6 @@ export abstract class CommandLineParameter { /** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */ public readonly shortName: string | undefined; - /** {@inheritDoc IBaseCommandLineDefinition.parameterGroup} */ - public readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; - /** {@inheritDoc IBaseCommandLineDefinition.description} */ public readonly description: string; @@ -73,7 +69,6 @@ export abstract class CommandLineParameter { public constructor(definition: IBaseCommandLineDefinition) { this.longName = definition.parameterLongName; this.shortName = definition.parameterShortName; - this.parameterGroup = definition.parameterGroup; this.description = definition.description; this.required = !!definition.required; this.environmentVariable = definition.environmentVariable; diff --git a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts index a5322d02708..3d7913efb2c 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { SCOPING_PARAMETER_GROUP } from '../Constants'; - /** * For use with CommandLineParser, this interface represents a generic command-line parameter * @@ -19,11 +17,6 @@ export interface IBaseCommandLineDefinition { */ parameterShortName?: string; - /** - * An optional parameter group name, shown when invoking the tool with "--help" - */ - parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; - /** * Documentation for the parameter that will be shown when invoking the tool with "--help" */ diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index e9f5fb874ea..a0bd8deb4d9 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -2,9 +2,7 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; - import { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; -import type { ICommandLineParserOptions } from './CommandLineParser'; /** * Options for the CommandLineAction constructor. @@ -92,8 +90,8 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * This is called internally by CommandLineParser.execute() * @internal */ - public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { - super._processParsedData(parserOptions, data); + public _processParsedData(data: ICommandLineParserData): void { + super._processParsedData(data); } /** diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 7e02763190b..48e0818d01f 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -2,8 +2,7 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; - -import type { +import { ICommandLineChoiceDefinition, ICommandLineChoiceListDefinition, ICommandLineIntegerDefinition, @@ -13,7 +12,6 @@ import type { ICommandLineStringListDefinition, ICommandLineRemainderDefinition } from '../parameters/CommandLineDefinition'; -import type { ICommandLineParserOptions } from './CommandLineParser'; import { CommandLineParameter, CommandLineParameterWithArgument, @@ -27,7 +25,6 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; import { CommandLineStringListParameter } from '../parameters/CommandLineStringListParameter'; import { CommandLineRemainder } from '../parameters/CommandLineRemainder'; -import { SCOPING_PARAMETER_GROUP } from '../Constants'; /** * This is the argparse result data object @@ -49,7 +46,6 @@ export abstract class CommandLineParameterProvider { private _parameters: CommandLineParameter[]; private _parametersByLongName: Map; - private _parameterGroupsByName: Map; private _parametersProcessed: boolean; private _remainder: CommandLineRemainder | undefined; @@ -57,8 +53,7 @@ export abstract class CommandLineParameterProvider { // Third party code should not inherit subclasses or call this constructor public constructor() { this._parameters = []; - this._parametersByLongName = new Map(); - this._parameterGroupsByName = new Map(); + this._parametersByLongName = new Map(); this._parametersProcessed = false; } @@ -212,7 +207,6 @@ export abstract class CommandLineParameterProvider { public getIntegerListParameter(parameterLongName: string): CommandLineIntegerListParameter { return this._getParameter(parameterLongName, CommandLineParameterKind.IntegerList); } - /** * Defines a command-line parameter whose argument is a single text string. * @@ -303,13 +297,6 @@ export abstract class CommandLineParameterProvider { return this._getArgumentParser().formatHelp(); } - /** - * Generates the command-line usage text. - */ - public renderUsageText(): string { - return this._getArgumentParser().formatUsage(); - } - /** * Returns a object which maps the long name of each parameter in this.parameters * to the stringified form of its value. This is useful for logging telemetry, but @@ -362,7 +349,7 @@ export abstract class CommandLineParameterProvider { protected abstract _getArgumentParser(): argparse.ArgumentParser; /** @internal */ - protected _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + protected _processParsedData(data: ICommandLineParserData): void { if (this._parametersProcessed) { throw new Error('Command Line Parser Data was already processed'); } @@ -380,8 +367,28 @@ export abstract class CommandLineParameterProvider { this._parametersProcessed = true; } - /** @internal */ - protected _defineParameter(parameter: CommandLineParameter): void { + private _generateKey(): string { + return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); + } + + private _getParameter( + parameterLongName: string, + expectedKind: CommandLineParameterKind + ): T { + const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); + if (!parameter) { + throw new Error(`The parameter "${parameterLongName}" is not defined`); + } + if (parameter.kind !== expectedKind) { + throw new Error( + `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + + ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` + ); + } + return parameter as T; + } + + private _defineParameter(parameter: CommandLineParameter): void { if (this._remainder) { throw new Error( 'defineCommandLineRemainder() was already called for this provider;' + @@ -448,32 +455,10 @@ export abstract class CommandLineParameterProvider { break; } - let argumentGroup: argparse.ArgumentGroup | undefined; - if (parameter.parameterGroup) { - argumentGroup = this._parameterGroupsByName.get(parameter.parameterGroup); - if (!argumentGroup) { - let parameterGroupName: string; - if (typeof parameter.parameterGroup === 'string') { - parameterGroupName = parameter.parameterGroup; - } else if (parameter.parameterGroup === SCOPING_PARAMETER_GROUP) { - parameterGroupName = 'scoping'; - } else { - throw new Error('Unexpected parameter group: ' + parameter.parameterGroup); - } - - argumentGroup = this._getArgumentParser().addArgumentGroup({ - title: `Optional ${parameterGroupName} arguments` - }); - this._parameterGroupsByName.set(parameter.parameterGroup, argumentGroup); - } - } else { - argumentGroup = this._getArgumentParser(); - } - - argumentGroup.addArgument(names, { ...argparseOptions }); - + const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); + argumentParser.addArgument(names, { ...argparseOptions }); if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { - argumentGroup.addArgument(parameter.undocumentedSynonyms, { + argumentParser.addArgument(parameter.undocumentedSynonyms, { ...argparseOptions, help: argparse.Const.SUPPRESS }); @@ -482,25 +467,4 @@ export abstract class CommandLineParameterProvider { this._parameters.push(parameter); this._parametersByLongName.set(parameter.longName, parameter); } - - private _generateKey(): string { - return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); - } - - private _getParameter( - parameterLongName: string, - expectedKind: CommandLineParameterKind - ): T { - const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); - if (!parameter) { - throw new Error(`The parameter "${parameterLongName}" is not defined`); - } - if (parameter.kind !== expectedKind) { - throw new Error( - `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + - ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` - ); - } - return parameter as T; - } } diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index 25fc95342b1..f14888886ec 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -24,12 +24,6 @@ export interface ICommandLineParserOptions { */ toolDescription: string; - /** - * An optional string to append at the end of the "--help" main page. If not provided, an epilog - * will be automatically generated based on the toolFilename. - */ - toolEpilog?: string; - /** * Set to true to auto-define a tab completion action. False by default. */ @@ -74,8 +68,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( - this._options.toolEpilog ?? - `For detailed help about a specific command, use: ${this._options.toolFilename} -h` + `For detailed help about a specific command, use: ${this._options.toolFilename} -h` ) }); @@ -201,21 +194,19 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { // 0=node.exe, 1=script name args = process.argv.slice(2); } - if (this.actions.length > 0 && args.length === 0) { - // Parsers that use actions should print help when 0 args are provided. Allow - // actionless parsers to continue on zero args. + if (args.length === 0) { this._argumentParser.printHelp(); return; } const data: ICommandLineParserData = this._argumentParser.parseArgs(args); - this._processParsedData(this._options, data); + this._processParsedData(data); for (const action of this._actions) { if (action.actionName === data.action) { this.selectedAction = action; - action._processParsedData(this._options, data); + action._processParsedData(data); break; } } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts deleted file mode 100644 index bdc10a516f8..00000000000 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { CommandLineAction, ICommandLineActionOptions } from './CommandLineAction'; -import { CommandLineParser, ICommandLineParserOptions } from './CommandLineParser'; -import { CommandLineParserExitError } from './CommandLineParserExitError'; -import type { CommandLineParameter } from '../parameters/BaseClasses'; -import type { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; -import { SCOPING_PARAMETER_GROUP } from '../Constants'; - -interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { - readonly actionOptions: ICommandLineActionOptions; - readonly unscopedActionParameters: ReadonlyArray; - readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; -} - -/** - * A CommandLineParser used exclusively to parse the scoped command-line parameters - * for a ScopedCommandLineAction. - */ -class InternalScopedCommandLineParser extends CommandLineParser { - private _internalOptions: IInternalScopedCommandLineParserOptions; - - public constructor(options: IInternalScopedCommandLineParserOptions) { - // We can run the parser directly because we are not going to use it for any other actions, - // so construct a special options object to make the "--help" text more useful. - const scopingArgs: string[] = []; - for (const parameter of options.unscopedActionParameters) { - parameter.appendToArgList(scopingArgs); - } - const unscopedToolName: string = `${options.toolFilename} ${options.actionOptions.actionName}`; - const scopedCommandLineParserOptions: ICommandLineParserOptions = { - toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, - toolDescription: options.actionOptions.documentation, - toolEpilog: `For more information on available unscoped parameters, use "${unscopedToolName} --help"`, - enableTabCompletionAction: false - }; - - super(scopedCommandLineParserOptions); - this._internalOptions = options; - this._internalOptions.onDefineScopedParameters(this); - } - - protected onDefineParameters(): void { - // No-op. Parameters are manually defined in the constructor. - } -} - -/** - * Represents a sub-command that is part of the CommandLineParser command-line. - * Applications should create subclasses of ScopedCommandLineAction corresponding to - * each action that they want to expose. - * - * The action name should be comprised of lower case words separated by hyphens - * or colons. The name should include an English verb (e.g. "deploy"). Use a - * hyphen to separate words (e.g. "upload-docs"). A group of related commands - * can be prefixed with a colon (e.g. "docs:generate", "docs:deploy", - * "docs:serve", etc). - * - * Scoped commands allow for different parameters to be specified for different - * provided scoping values. For example, the "scoped-action --scope A" command - * may allow for different scoped arguments to be specified than the "scoped-action - * --scope B" command. - * - * Scoped arguments are specified after the "--" pseudo-argument. For example, - * "scoped-action --scope A -- --scopedFoo --scopedBar". - * - * @public - */ -export abstract class ScopedCommandLineAction extends CommandLineAction { - private _options: ICommandLineActionOptions; - private _scopingParameters: CommandLineParameter[]; - private _unscopedParserOptions: ICommandLineParserOptions | undefined; - private _scopedCommandLineParser: InternalScopedCommandLineParser | undefined; - - /** - * The required group name to apply to all scoping parameters. At least one parameter - * must be defined with this group name. - */ - public static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP = SCOPING_PARAMETER_GROUP; - - public constructor(options: ICommandLineActionOptions) { - super(options); - - this._options = options; - this._scopingParameters = []; - } - - /** - * {@inheritDoc CommandLineParameterProvider.parameters} - */ - public get parameters(): ReadonlyArray { - if (this._scopedCommandLineParser) { - return [...super.parameters, ...this._scopedCommandLineParser.parameters]; - } else { - return super.parameters; - } - } - - /** - * {@inheritdoc CommandLineAction._processParsedData} - * @internal - */ - public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { - // override - super._processParsedData(parserOptions, data); - - this._unscopedParserOptions = parserOptions; - - // Generate the scoped parser using the parent parser information. We can only create this after we - // have parsed the data, since the parameter values are used during construction. - this._scopedCommandLineParser = new InternalScopedCommandLineParser({ - ...parserOptions, - actionOptions: this._options, - unscopedActionParameters: this.parameters, - onDefineScopedParameters: this.onDefineScopedParameters.bind(this) - }); - } - - /** - * {@inheritdoc CommandLineAction._execute} - * @internal - */ - public async _execute(): Promise { - // override - if (!this._unscopedParserOptions || !this._scopedCommandLineParser) { - throw new Error('The CommandLineAction parameters must be processed before execution.'); - } - if (!this.remainder) { - throw new Error('CommandLineAction.onDefineParameters must be called before execution.'); - } - - // The '--' argument is required to separate the action parameters from the scoped parameters, - // so it needs to be trimmed. If remainder values are provided but no '--' is found, then throw. - const scopedArgs: string[] = []; - if (this.remainder.values.length) { - if (this.remainder.values[0] !== '--') { - // Immitate argparse behavior and log out usage text before throwing. - console.log(this.renderUsageText()); - throw new CommandLineParserExitError( - // argparse sets exit code 2 for invalid arguments - 2, - // model the message off of the built-in "unrecognized arguments" message - `${this._unscopedParserOptions.toolFilename} ${this.actionName}: error: Unrecognized ` + - `arguments: ${this.remainder.values[0]}.` - ); - } - scopedArgs.push(...this.remainder.values.slice(1)); - } - - // Call the scoped parser using only the scoped args. - await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); - await super._execute(); - return; - } - - /** - * {@inheritdoc CommandLineParameterProvider.onDefineParameters} - */ - protected onDefineParameters(): void { - this.onDefineUnscopedParameters(); - - if (!this._scopingParameters.length) { - throw new Error( - 'No scoping parameters defined. At least one scoping parameter must be defined. ' + - 'Scoping parameters are defined by setting the parameterGroupName to ' + - 'ScopedCommandLineAction.ScopingParameterGroupName.' - ); - } - if (this.remainder) { - throw new Error( - 'Unscoped remainder parameters are not allowed. Remainder parameters can only be defined on ' + - 'the scoped parameter provider in onDefineScopedParameters().' - ); - } - - // Consume the remainder of the command-line, which will later be passed the scoped parser. - // This will also prevent developers from calling this.defineCommandLineRemainder(...) since - // we will have already defined it. - this.defineCommandLineRemainder({ - description: - 'Scoped parameters. Must be prefixed with "--", ex. "-- --scopedParameter ' + - 'foo --scopedFlag". For more information on available scoped parameters, use "-- --help".' - }); - } - - /** - * Retrieves the scoped CommandLineParser, which is populated after the ScopedCommandLineAction is executed. - * @internal - */ - protected _getScopedCommandLineParser(): CommandLineParser { - if (!this._scopedCommandLineParser) { - throw new Error('The scoped CommandLineParser is only populated after the action is executed.'); - } - return this._scopedCommandLineParser; - } - - /** @internal */ - protected _defineParameter(parameter: CommandLineParameter): void { - super._defineParameter(parameter); - if (parameter.parameterGroup === ScopedCommandLineAction.ScopingParameterGroup) { - this._scopingParameters.push(parameter); - } - } - - /** - * The child class should implement this hook to define its unscoped command-line parameters, - * e.g. by calling defineFlagParameter(). At least one scoping parameter must be defined. - * Scoping parameters are defined by setting the parameterGroupName to - * ScopedCommandLineAction.ScopingParameterGroupName. - */ - protected abstract onDefineUnscopedParameters(): void; - - /** - * The child class should implement this hook to define its scoped command-line - * parameters, e.g. by calling scopedParameterProvider.defineFlagParameter(). These - * parameters will only be available if the action is invoked with a scope. - * - * @remarks - * onDefineScopedParameters is called after the unscoped parameters have been parsed. - * The values they provide can be used to vary the defined scope parameters. - */ - protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; - - /** - * {@inheritDoc CommandLineAction.onExecute} - */ - protected abstract onExecute(): Promise; -} diff --git a/libraries/ts-command-line/src/test/ActionlessParser.test.ts b/libraries/ts-command-line/src/test/ActionlessParser.test.ts index 71c0e7edd16..dea5cdc0a64 100644 --- a/libraries/ts-command-line/src/test/ActionlessParser.test.ts +++ b/libraries/ts-command-line/src/test/ActionlessParser.test.ts @@ -6,7 +6,6 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter class TestCommandLine extends CommandLineParser { public flag!: CommandLineFlagParameter; - public done: boolean = false; public constructor() { super({ @@ -15,11 +14,6 @@ class TestCommandLine extends CommandLineParser { }); } - protected async onExecute(): Promise { - await super.onExecute(); - this.done = true; - } - protected onDefineParameters(): void { this.flag = this.defineFlagParameter({ parameterLongName: '--flag', @@ -29,22 +23,11 @@ class TestCommandLine extends CommandLineParser { } describe(`Actionless ${CommandLineParser.name}`, () => { - it('parses an empty arg list', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - - await commandLineParser.execute([]); - - expect(commandLineParser.done).toBe(true); - expect(commandLineParser.selectedAction).toBeUndefined(); - expect(commandLineParser.flag.value).toBe(false); - }); - it('parses a flag', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); await commandLineParser.execute(['--flag']); - expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); }); @@ -58,7 +41,6 @@ describe(`Actionless ${CommandLineParser.name}`, () => { await commandLineParser.execute(['--flag', 'the', 'remaining', 'args']); - expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); expect(commandLineParser.remainder!.values).toEqual(['the', 'remaining', 'args']); diff --git a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts index af389928f9a..08c104e9d90 100644 --- a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts @@ -361,34 +361,31 @@ describe(CommandLineParameter.name, () => { return commandLineParser; } - it('raises an error if env var value is not valid json', async () => { + it('raises an error if env var value is not valid json', () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[u'; - await expect( - commandLineParser.executeWithoutErrorHandling(args) - ).rejects.toThrowErrorMatchingSnapshot(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is json containing non-scalars', async () => { + it('raises an error if env var value is json containing non-scalars', () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[{}]'; - await expect( - commandLineParser.executeWithoutErrorHandling(args) - ).rejects.toThrowErrorMatchingSnapshot(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is not a valid choice', async () => { + it('raises an error if env var value is not a valid choice', () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = 'oblong'; - await expect( - commandLineParser.executeWithoutErrorHandling(args) - ).rejects.toThrowErrorMatchingSnapshot(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts index 0becb8dede7..62bd8cdc949 100644 --- a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts @@ -73,25 +73,4 @@ describe(CommandLineRemainder.name, () => { expect(copiedArgs).toMatchSnapshot(); }); - - it('parses an action input with remainder flagged options', async () => { - const commandLineParser: CommandLineParser = createParser(); - const action: CommandLineAction = commandLineParser.getAction('run'); - const args: string[] = ['run', '--title', 'The title', '--', '--the', 'remaining', '--args']; - - await commandLineParser.execute(args); - - expect(commandLineParser.selectedAction).toBe(action); - - const copiedArgs: string[] = []; - for (const parameter of action.parameters) { - copiedArgs.push(`### ${parameter.longName} output: ###`); - parameter.appendToArgList(copiedArgs); - } - - copiedArgs.push(`### remainder output: ###`); - action.remainder!.appendToArgList(copiedArgs); - - expect(copiedArgs).toMatchSnapshot(); - }); }); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts deleted file mode 100644 index 05336946ef4..00000000000 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as colors from 'colors'; - -import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; -import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; -import { CommandLineParser } from '../providers/CommandLineParser'; -import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; -import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter'; - -class TestScopedAction extends ScopedCommandLineAction { - public done: boolean = false; - public scopedValue: string | undefined; - private _verboseArg!: CommandLineFlagParameter; - private _scopeArg!: CommandLineStringParameter; - private _scopedArg: CommandLineStringParameter | undefined; - - public constructor() { - super({ - actionName: 'scoped-action', - summary: 'does the scoped action', - documentation: 'a longer description' - }); - } - - protected async onExecute(): Promise { - if (this._scopedArg) { - expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); - this.scopedValue = this._scopedArg.value; - } - this.done = true; - } - - protected onDefineUnscopedParameters(): void { - this._verboseArg = this.defineFlagParameter({ - parameterLongName: '--verbose', - description: 'A flag parameter.' - }); - - this._scopeArg = this.defineStringParameter({ - parameterLongName: '--scope', - parameterGroup: ScopedCommandLineAction.ScopingParameterGroup, - argumentName: 'SCOPE', - description: 'The scope' - }); - } - - protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { - if (this._scopeArg.value) { - this._scopedArg = scopedParameterProvider.defineStringParameter({ - parameterLongName: `--scoped-${this._scopeArg.value}`, - argumentName: 'SCOPED', - description: 'The scoped argument.' - }); - } - } -} - -class TestCommandLine extends CommandLineParser { - public constructor() { - super({ - toolFilename: 'example', - toolDescription: 'An example project' - }); - - this.addAction(new TestScopedAction()); - } - - protected onDefineParameters(): void { - // no parameters - } -} - -describe(CommandLineParser.name, () => { - it('throws on unknown scoped arg', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; - - await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); - }); - - it('throws on missing positional arg divider with unknown positional args', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const args: string[] = ['scoped-action', '--scope', 'foo', 'bar']; - - await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); - }); - - it('executes a scoped action', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - - await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - - expect(commandLineParser.selectedAction).toBeDefined(); - expect(commandLineParser.selectedAction!.actionName).toEqual('scoped-action'); - - const action: TestScopedAction = commandLineParser.selectedAction as TestScopedAction; - expect(action.done).toBe(true); - expect(action.scopedValue).toBe('bar'); - }); - - it('prints the action help', () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const helpText: string = colors.stripColors( - commandLineParser.getAction('scoped-action').renderHelpText() - ); - expect(helpText).toMatchSnapshot(); - }); - - it('prints the scoped action help', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action to populate the help text. - await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - const scopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = - commandLineParser.getAction('scoped-action') as TestScopedAction & { - _getScopedCommandLineParser(): CommandLineParser; - }; - const scopedCommandLineParser: CommandLineParser = scopedAction._getScopedCommandLineParser(); - const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); - expect(helpText).toMatchSnapshot(); - }); - - it('prints the unscoped action parameter map', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--verbose']); - const scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; - expect(scopedAction.done).toBe(true); - expect(scopedAction.parameters.length).toBe(2); - const parameterStringMap: Record = scopedAction.getParameterStringMap(); - expect(parameterStringMap).toMatchSnapshot(); - }); - - it('prints the scoped action parameter map', async () => { - let commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--scope', 'foo']); - let scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; - expect(scopedAction.done).toBe(true); - expect(scopedAction.parameters.length).toBe(3); - let parameterStringMap: Record = scopedAction.getParameterStringMap(); - expect(parameterStringMap).toMatchSnapshot(); - - commandLineParser = new TestCommandLine(); - // Execute the parser in order to populate the scoped action - await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); - scopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; - expect(scopedAction.done).toBe(true); - expect(scopedAction.parameters.length).toBe(3); - parameterStringMap = scopedAction.getParameterStringMap(); - expect(parameterStringMap).toMatchSnapshot(); - }); -}); diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap index 1250aa3605e..e11dc7a569d 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap @@ -12,19 +12,6 @@ Array [ ] `; -exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` -Array [ - "### --title output: ###", - "--title", - "The title", - "### remainder output: ###", - "--", - "--the", - "remaining", - "--args", -] -`; - exports[`CommandLineRemainder prints the action help 1`] = ` "usage: example run [-h] [--title TEXT] ... diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap deleted file mode 100644 index ca696e05d3d..00000000000 --- a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CommandLineParser prints the action help 1`] = ` -"usage: example scoped-action [-h] [--verbose] [--scope SCOPE] ... - -a longer description - -Positional arguments: - \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- - --scopedParameter foo --scopedFlag\\". For more information on - available scoped parameters, use \\"-- --help\\". - -Optional arguments: - -h, --help Show this help message and exit. - --verbose A flag parameter. - -Optional scoping arguments: - --scope SCOPE The scope -" -`; - -exports[`CommandLineParser prints the scoped action help 1`] = ` -"usage: example scoped-action --scope foo -- [-h] [--scoped-foo SCOPED] - -a longer description - -Optional arguments: - -h, --help Show this help message and exit. - --scoped-foo SCOPED The scoped argument. - -For more information on available unscoped parameters, use \\"example -scoped-action --help\\" -" -`; - -exports[`CommandLineParser prints the scoped action parameter map 1`] = ` -Object { - "--scope": "\\"foo\\"", - "--scoped-foo": undefined, - "--verbose": "false", -} -`; - -exports[`CommandLineParser prints the scoped action parameter map 2`] = ` -Object { - "--scope": "\\"foo\\"", - "--scoped-foo": "\\"bar\\"", - "--verbose": "false", -} -`; - -exports[`CommandLineParser prints the unscoped action parameter map 1`] = ` -Object { - "--scope": undefined, - "--verbose": "true", -} -`; - -exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; - -exports[`CommandLineParser throws on unknown scoped arg 1`] = ` -"example scoped-action --scope foo --: error: Unrecognized arguments: --scoped-bar baz. -" -`; From 02d9a97be1a373bb80387f4fe5acae3598c34d73 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Wed, 20 Apr 2022 14:37:34 -0700 Subject: [PATCH 07/73] Update argparse version Add new remainder test Add ScopedCommandLineParser Revert "Update argparse version" This reverts commit 6f917d351eede7297e65132dbb3f33a3622be6c3. Cleanup More cleanup Rush change Updated docstring Tweaks to allow for unscoped calls to scoping actions, allowing the developer to decide if the scope is required Apply suggestions from code review Co-authored-by: Ian Clanton-Thuon Call the onExecute directly Use a symbol for the 'scoping' parameter group Break a circular reference. Fixes for writing help and continuing to execute Add missing onExecute Nit: import order --- ...de-ScopedCommandLine_2022-04-21-22-00.json | 10 + common/reviews/api/ts-command-line.api.md | 29 +- libraries/ts-command-line/src/Constants.ts | 2 + libraries/ts-command-line/src/index.ts | 5 +- .../src/parameters/BaseClasses.ts | 5 + .../src/parameters/CommandLineDefinition.ts | 7 + .../src/providers/CommandLineAction.ts | 6 +- .../providers/CommandLineParameterProvider.ts | 92 +++++-- .../src/providers/CommandLineParser.ts | 17 +- .../src/providers/ScopedCommandLineAction.ts | 248 ++++++++++++++++++ .../src/test/ActionlessParser.test.ts | 18 ++ .../src/test/CommandLineParameter.test.ts | 21 +- .../src/test/CommandLineRemainder.test.ts | 21 ++ .../src/test/ScopedCommandLineAction.test.ts | 154 +++++++++++ .../CommandLineRemainder.test.ts.snap | 13 + .../ScopedCommandLineAction.test.ts.snap | 64 +++++ 16 files changed, 664 insertions(+), 48 deletions(-) create mode 100644 common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json create mode 100644 libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts create mode 100644 libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts create mode 100644 libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap diff --git a/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json new file mode 100644 index 00000000000..7f0cc08de2a --- /dev/null +++ b/common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/ts-command-line", + "comment": "Add ScopedCommandLineAction class, which allows for the definition of actions that have dynamic arguments whose definition depends on a provided scope. See https://github.com/microsoft/rushstack/pull/3364", + "type": "minor" + } + ], + "packageName": "@rushstack/ts-command-line" +} \ No newline at end of file diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index aea2b69eec2..c9964e7a8ab 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -20,7 +20,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { protected abstract onDefineParameters(): void; protected abstract onExecute(): Promise; // @internal - _processParsedData(data: _ICommandLineParserData): void; + _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; readonly summary: string; } @@ -115,6 +115,7 @@ export abstract class CommandLineParameter { _getSupplementaryNotes(supplementaryNotes: string[]): void; abstract get kind(): CommandLineParameterKind; readonly longName: string; + readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; // @internal _parserKey: string | undefined; protected reportInvalidData(data: any): never; @@ -148,6 +149,8 @@ export abstract class CommandLineParameterProvider { defineFlagParameter(definition: ICommandLineFlagDefinition): CommandLineFlagParameter; defineIntegerListParameter(definition: ICommandLineIntegerListDefinition): CommandLineIntegerListParameter; defineIntegerParameter(definition: ICommandLineIntegerDefinition): CommandLineIntegerParameter; + // @internal (undocumented) + protected _defineParameter(parameter: CommandLineParameter): void; defineStringListParameter(definition: ICommandLineStringListDefinition): CommandLineStringListParameter; defineStringParameter(definition: ICommandLineStringDefinition): CommandLineStringParameter; // @internal @@ -164,9 +167,10 @@ export abstract class CommandLineParameterProvider { get parameters(): ReadonlyArray; get parametersProcessed(): boolean; // @internal (undocumented) - protected _processParsedData(data: _ICommandLineParserData): void; + protected _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; get remainder(): CommandLineRemainder | undefined; renderHelpText(): string; + renderUsageText(): string; } // @public @@ -249,6 +253,7 @@ export class DynamicCommandLineParser extends CommandLineParser { export interface IBaseCommandLineDefinition { description: string; environmentVariable?: string; + parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; parameterLongName: string; parameterShortName?: string; required?: boolean; @@ -306,6 +311,7 @@ export interface _ICommandLineParserData { export interface ICommandLineParserOptions { enableTabCompletionAction?: boolean; toolDescription: string; + toolEpilog?: string; toolFilename: string; } @@ -323,4 +329,23 @@ export interface ICommandLineStringDefinition extends IBaseCommandLineDefinition export interface ICommandLineStringListDefinition extends IBaseCommandLineDefinitionWithArgument { } +// @public +export abstract class ScopedCommandLineAction extends CommandLineAction { + constructor(options: ICommandLineActionOptions); + // @internal (undocumented) + protected _defineParameter(parameter: CommandLineParameter): void; + // @internal + _execute(): Promise; + // @internal + protected _getScopedCommandLineParser(): CommandLineParser; + protected onDefineParameters(): void; + protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; + protected abstract onDefineUnscopedParameters(): void; + protected abstract onExecute(): Promise; + get parameters(): ReadonlyArray; + // @internal + _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; + static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP; +} + ``` diff --git a/libraries/ts-command-line/src/Constants.ts b/libraries/ts-command-line/src/Constants.ts index 7a5a285280e..5b2d48b00c8 100644 --- a/libraries/ts-command-line/src/Constants.ts +++ b/libraries/ts-command-line/src/Constants.ts @@ -12,3 +12,5 @@ export const enum CommandLineConstants { */ TabCompletionActionName = 'tab-complete' } + +export const SCOPING_PARAMETER_GROUP: unique symbol = Symbol('scoping'); diff --git a/libraries/ts-command-line/src/index.ts b/libraries/ts-command-line/src/index.ts index 2c82b011bc0..1cd47635a41 100644 --- a/libraries/ts-command-line/src/index.ts +++ b/libraries/ts-command-line/src/index.ts @@ -8,6 +8,8 @@ */ export { CommandLineAction, ICommandLineActionOptions } from './providers/CommandLineAction'; +export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; +export { ScopedCommandLineAction } from './providers/ScopedCommandLineAction'; export { IBaseCommandLineDefinition, @@ -43,9 +45,6 @@ export { } from './providers/CommandLineParameterProvider'; export { ICommandLineParserOptions, CommandLineParser } from './providers/CommandLineParser'; - -export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction'; - export { DynamicCommandLineParser } from './providers/DynamicCommandLineParser'; export { CommandLineConstants } from './Constants'; diff --git a/libraries/ts-command-line/src/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 8b61cb49a08..1950696fae6 100644 --- a/libraries/ts-command-line/src/parameters/BaseClasses.ts +++ b/libraries/ts-command-line/src/parameters/BaseClasses.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import type { SCOPING_PARAMETER_GROUP } from '../Constants'; import { IBaseCommandLineDefinition, IBaseCommandLineDefinitionWithArgument } from './CommandLineDefinition'; /** @@ -53,6 +54,9 @@ export abstract class CommandLineParameter { /** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */ public readonly shortName: string | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.parameterGroup} */ + public readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.description} */ public readonly description: string; @@ -69,6 +73,7 @@ export abstract class CommandLineParameter { public constructor(definition: IBaseCommandLineDefinition) { this.longName = definition.parameterLongName; this.shortName = definition.parameterShortName; + this.parameterGroup = definition.parameterGroup; this.description = definition.description; this.required = !!definition.required; this.environmentVariable = definition.environmentVariable; diff --git a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts index 3d7913efb2c..a5322d02708 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import type { SCOPING_PARAMETER_GROUP } from '../Constants'; + /** * For use with CommandLineParser, this interface represents a generic command-line parameter * @@ -17,6 +19,11 @@ export interface IBaseCommandLineDefinition { */ parameterShortName?: string; + /** + * An optional parameter group name, shown when invoking the tool with "--help" + */ + parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; + /** * Documentation for the parameter that will be shown when invoking the tool with "--help" */ diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index a0bd8deb4d9..e9f5fb874ea 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -2,7 +2,9 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; + import { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; +import type { ICommandLineParserOptions } from './CommandLineParser'; /** * Options for the CommandLineAction constructor. @@ -90,8 +92,8 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * This is called internally by CommandLineParser.execute() * @internal */ - public _processParsedData(data: ICommandLineParserData): void { - super._processParsedData(data); + public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + super._processParsedData(parserOptions, data); } /** diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 48e0818d01f..7e02763190b 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -2,7 +2,8 @@ // See LICENSE in the project root for license information. import * as argparse from 'argparse'; -import { + +import type { ICommandLineChoiceDefinition, ICommandLineChoiceListDefinition, ICommandLineIntegerDefinition, @@ -12,6 +13,7 @@ import { ICommandLineStringListDefinition, ICommandLineRemainderDefinition } from '../parameters/CommandLineDefinition'; +import type { ICommandLineParserOptions } from './CommandLineParser'; import { CommandLineParameter, CommandLineParameterWithArgument, @@ -25,6 +27,7 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; import { CommandLineStringListParameter } from '../parameters/CommandLineStringListParameter'; import { CommandLineRemainder } from '../parameters/CommandLineRemainder'; +import { SCOPING_PARAMETER_GROUP } from '../Constants'; /** * This is the argparse result data object @@ -46,6 +49,7 @@ export abstract class CommandLineParameterProvider { private _parameters: CommandLineParameter[]; private _parametersByLongName: Map; + private _parameterGroupsByName: Map; private _parametersProcessed: boolean; private _remainder: CommandLineRemainder | undefined; @@ -53,7 +57,8 @@ export abstract class CommandLineParameterProvider { // Third party code should not inherit subclasses or call this constructor public constructor() { this._parameters = []; - this._parametersByLongName = new Map(); + this._parametersByLongName = new Map(); + this._parameterGroupsByName = new Map(); this._parametersProcessed = false; } @@ -207,6 +212,7 @@ export abstract class CommandLineParameterProvider { public getIntegerListParameter(parameterLongName: string): CommandLineIntegerListParameter { return this._getParameter(parameterLongName, CommandLineParameterKind.IntegerList); } + /** * Defines a command-line parameter whose argument is a single text string. * @@ -297,6 +303,13 @@ export abstract class CommandLineParameterProvider { return this._getArgumentParser().formatHelp(); } + /** + * Generates the command-line usage text. + */ + public renderUsageText(): string { + return this._getArgumentParser().formatUsage(); + } + /** * Returns a object which maps the long name of each parameter in this.parameters * to the stringified form of its value. This is useful for logging telemetry, but @@ -349,7 +362,7 @@ export abstract class CommandLineParameterProvider { protected abstract _getArgumentParser(): argparse.ArgumentParser; /** @internal */ - protected _processParsedData(data: ICommandLineParserData): void { + protected _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { if (this._parametersProcessed) { throw new Error('Command Line Parser Data was already processed'); } @@ -367,28 +380,8 @@ export abstract class CommandLineParameterProvider { this._parametersProcessed = true; } - private _generateKey(): string { - return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); - } - - private _getParameter( - parameterLongName: string, - expectedKind: CommandLineParameterKind - ): T { - const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); - if (!parameter) { - throw new Error(`The parameter "${parameterLongName}" is not defined`); - } - if (parameter.kind !== expectedKind) { - throw new Error( - `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + - ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` - ); - } - return parameter as T; - } - - private _defineParameter(parameter: CommandLineParameter): void { + /** @internal */ + protected _defineParameter(parameter: CommandLineParameter): void { if (this._remainder) { throw new Error( 'defineCommandLineRemainder() was already called for this provider;' + @@ -455,10 +448,32 @@ export abstract class CommandLineParameterProvider { break; } - const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); - argumentParser.addArgument(names, { ...argparseOptions }); + let argumentGroup: argparse.ArgumentGroup | undefined; + if (parameter.parameterGroup) { + argumentGroup = this._parameterGroupsByName.get(parameter.parameterGroup); + if (!argumentGroup) { + let parameterGroupName: string; + if (typeof parameter.parameterGroup === 'string') { + parameterGroupName = parameter.parameterGroup; + } else if (parameter.parameterGroup === SCOPING_PARAMETER_GROUP) { + parameterGroupName = 'scoping'; + } else { + throw new Error('Unexpected parameter group: ' + parameter.parameterGroup); + } + + argumentGroup = this._getArgumentParser().addArgumentGroup({ + title: `Optional ${parameterGroupName} arguments` + }); + this._parameterGroupsByName.set(parameter.parameterGroup, argumentGroup); + } + } else { + argumentGroup = this._getArgumentParser(); + } + + argumentGroup.addArgument(names, { ...argparseOptions }); + if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { - argumentParser.addArgument(parameter.undocumentedSynonyms, { + argumentGroup.addArgument(parameter.undocumentedSynonyms, { ...argparseOptions, help: argparse.Const.SUPPRESS }); @@ -467,4 +482,25 @@ export abstract class CommandLineParameterProvider { this._parameters.push(parameter); this._parametersByLongName.set(parameter.longName, parameter); } + + private _generateKey(): string { + return 'key_' + (CommandLineParameterProvider._keyCounter++).toString(); + } + + private _getParameter( + parameterLongName: string, + expectedKind: CommandLineParameterKind + ): T { + const parameter: CommandLineParameter | undefined = this._parametersByLongName.get(parameterLongName); + if (!parameter) { + throw new Error(`The parameter "${parameterLongName}" is not defined`); + } + if (parameter.kind !== expectedKind) { + throw new Error( + `The parameter "${parameterLongName}" is of type "${CommandLineParameterKind[parameter.kind]}"` + + ` whereas the caller was expecting "${CommandLineParameterKind[expectedKind]}".` + ); + } + return parameter as T; + } } diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index f14888886ec..25fc95342b1 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -24,6 +24,12 @@ export interface ICommandLineParserOptions { */ toolDescription: string; + /** + * An optional string to append at the end of the "--help" main page. If not provided, an epilog + * will be automatically generated based on the toolFilename. + */ + toolEpilog?: string; + /** * Set to true to auto-define a tab completion action. False by default. */ @@ -68,7 +74,8 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( - `For detailed help about a specific command, use: ${this._options.toolFilename} -h` + this._options.toolEpilog ?? + `For detailed help about a specific command, use: ${this._options.toolFilename} -h` ) }); @@ -194,19 +201,21 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { // 0=node.exe, 1=script name args = process.argv.slice(2); } - if (args.length === 0) { + if (this.actions.length > 0 && 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; } const data: ICommandLineParserData = this._argumentParser.parseArgs(args); - this._processParsedData(data); + this._processParsedData(this._options, data); for (const action of this._actions) { if (action.actionName === data.action) { this.selectedAction = action; - action._processParsedData(data); + action._processParsedData(this._options, data); break; } } diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts new file mode 100644 index 00000000000..20c67323f41 --- /dev/null +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { SCOPING_PARAMETER_GROUP } from '../Constants'; +import { CommandLineAction, ICommandLineActionOptions } from './CommandLineAction'; +import { CommandLineParser, ICommandLineParserOptions } from './CommandLineParser'; +import { CommandLineParserExitError } from './CommandLineParserExitError'; +import type { CommandLineParameter } from '../parameters/BaseClasses'; +import type { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; + +interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { + readonly actionOptions: ICommandLineActionOptions; + readonly unscopedActionParameters: ReadonlyArray; + readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; +} + +/** + * A CommandLineParser used exclusively to parse the scoped command-line parameters + * for a ScopedCommandLineAction. + */ +class InternalScopedCommandLineParser extends CommandLineParser { + private _canExecute: boolean; + private _internalOptions: IInternalScopedCommandLineParserOptions; + + public get canExecute(): boolean { + return this._canExecute; + } + + public constructor(options: IInternalScopedCommandLineParserOptions) { + // We can run the parser directly because we are not going to use it for any other actions, + // so construct a special options object to make the "--help" text more useful. + const scopingArgs: string[] = []; + for (const parameter of options.unscopedActionParameters) { + parameter.appendToArgList(scopingArgs); + } + const unscopedToolName: string = `${options.toolFilename} ${options.actionOptions.actionName}`; + const scopedCommandLineParserOptions: ICommandLineParserOptions = { + toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, + toolDescription: options.actionOptions.documentation, + toolEpilog: `For more information on available unscoped parameters, use "${unscopedToolName} --help"`, + enableTabCompletionAction: false + }; + + super(scopedCommandLineParserOptions); + this._canExecute = false; + this._internalOptions = options; + this._internalOptions.onDefineScopedParameters(this); + } + + protected onDefineParameters(): void { + // No-op. Parameters are manually defined in the constructor. + } + + protected async onExecute(): Promise { + // override + // Only set if we made it this far, which may not be the case if an error occurred or + // if '--help' was specified. + this._canExecute = true; + } +} + +/** + * Represents a sub-command that is part of the CommandLineParser command-line. + * Applications should create subclasses of ScopedCommandLineAction corresponding to + * each action that they want to expose. + * + * The action name should be comprised of lower case words separated by hyphens + * or colons. The name should include an English verb (e.g. "deploy"). Use a + * hyphen to separate words (e.g. "upload-docs"). A group of related commands + * can be prefixed with a colon (e.g. "docs:generate", "docs:deploy", + * "docs:serve", etc). + * + * Scoped commands allow for different parameters to be specified for different + * provided scoping values. For example, the "scoped-action --scope A" command + * may allow for different scoped arguments to be specified than the "scoped-action + * --scope B" command. + * + * Scoped arguments are specified after the "--" pseudo-argument. For example, + * "scoped-action --scope A -- --scopedFoo --scopedBar". + * + * @public + */ +export abstract class ScopedCommandLineAction extends CommandLineAction { + private _options: ICommandLineActionOptions; + private _scopingParameters: CommandLineParameter[]; + private _unscopedParserOptions: ICommandLineParserOptions | undefined; + private _scopedCommandLineParser: InternalScopedCommandLineParser | undefined; + + /** + * The required group name to apply to all scoping parameters. At least one parameter + * must be defined with this group name. + */ + public static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP = SCOPING_PARAMETER_GROUP; + + public constructor(options: ICommandLineActionOptions) { + super(options); + + this._options = options; + this._scopingParameters = []; + } + + /** + * {@inheritDoc CommandLineParameterProvider.parameters} + */ + public get parameters(): ReadonlyArray { + if (this._scopedCommandLineParser) { + return [...super.parameters, ...this._scopedCommandLineParser.parameters]; + } else { + return super.parameters; + } + } + + /** + * {@inheritdoc CommandLineAction._processParsedData} + * @internal + */ + public _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void { + // override + super._processParsedData(parserOptions, data); + + this._unscopedParserOptions = parserOptions; + + // Generate the scoped parser using the parent parser information. We can only create this after we + // have parsed the data, since the parameter values are used during construction. + this._scopedCommandLineParser = new InternalScopedCommandLineParser({ + ...parserOptions, + actionOptions: this._options, + unscopedActionParameters: this.parameters, + onDefineScopedParameters: this.onDefineScopedParameters.bind(this) + }); + } + + /** + * {@inheritdoc CommandLineAction._execute} + * @internal + */ + public async _execute(): Promise { + // override + if (!this._unscopedParserOptions || !this._scopedCommandLineParser) { + throw new Error('The CommandLineAction parameters must be processed before execution.'); + } + if (!this.remainder) { + throw new Error('CommandLineAction.onDefineParameters must be called before execution.'); + } + + // The '--' argument is required to separate the action parameters from the scoped parameters, + // so it needs to be trimmed. If remainder values are provided but no '--' is found, then throw. + const scopedArgs: string[] = []; + if (this.remainder.values.length) { + if (this.remainder.values[0] !== '--') { + // Immitate argparse behavior and log out usage text before throwing. + console.log(this.renderUsageText()); + throw new CommandLineParserExitError( + // argparse sets exit code 2 for invalid arguments + 2, + // model the message off of the built-in "unrecognized arguments" message + `${this._unscopedParserOptions.toolFilename} ${this.actionName}: error: Unrecognized ` + + `arguments: ${this.remainder.values[0]}.` + ); + } + scopedArgs.push(...this.remainder.values.slice(1)); + } + + // Call the scoped parser using only the scoped args to handle parsing + await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + + // Only call execute if the parser reached the execute stage. This may not be true if + // the parser exited early due to a specified '--help' parameter. + if (this._scopedCommandLineParser.canExecute) { + await super._execute(); + } + + return; + } + + /** + * {@inheritdoc CommandLineParameterProvider.onDefineParameters} + */ + protected onDefineParameters(): void { + this.onDefineUnscopedParameters(); + + if (!this._scopingParameters.length) { + throw new Error( + 'No scoping parameters defined. At least one scoping parameter must be defined. ' + + 'Scoping parameters are defined by setting the parameterGroupName to ' + + 'ScopedCommandLineAction.ScopingParameterGroupName.' + ); + } + if (this.remainder) { + throw new Error( + 'Unscoped remainder parameters are not allowed. Remainder parameters can only be defined on ' + + 'the scoped parameter provider in onDefineScopedParameters().' + ); + } + + // Consume the remainder of the command-line, which will later be passed the scoped parser. + // This will also prevent developers from calling this.defineCommandLineRemainder(...) since + // we will have already defined it. + this.defineCommandLineRemainder({ + description: + 'Scoped parameters. Must be prefixed with "--", ex. "-- --scopedParameter ' + + 'foo --scopedFlag". For more information on available scoped parameters, use "-- --help".' + }); + } + + /** + * Retrieves the scoped CommandLineParser, which is populated after the ScopedCommandLineAction is executed. + * @internal + */ + protected _getScopedCommandLineParser(): CommandLineParser { + if (!this._scopedCommandLineParser) { + throw new Error('The scoped CommandLineParser is only populated after the action is executed.'); + } + return this._scopedCommandLineParser; + } + + /** @internal */ + protected _defineParameter(parameter: CommandLineParameter): void { + super._defineParameter(parameter); + if (parameter.parameterGroup === ScopedCommandLineAction.ScopingParameterGroup) { + this._scopingParameters.push(parameter); + } + } + + /** + * The child class should implement this hook to define its unscoped command-line parameters, + * e.g. by calling defineFlagParameter(). At least one scoping parameter must be defined. + * Scoping parameters are defined by setting the parameterGroupName to + * ScopedCommandLineAction.ScopingParameterGroupName. + */ + protected abstract onDefineUnscopedParameters(): void; + + /** + * The child class should implement this hook to define its scoped command-line + * parameters, e.g. by calling scopedParameterProvider.defineFlagParameter(). These + * parameters will only be available if the action is invoked with a scope. + * + * @remarks + * onDefineScopedParameters is called after the unscoped parameters have been parsed. + * The values they provide can be used to vary the defined scope parameters. + */ + protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void; + + /** + * {@inheritDoc CommandLineAction.onExecute} + */ + protected abstract onExecute(): Promise; +} diff --git a/libraries/ts-command-line/src/test/ActionlessParser.test.ts b/libraries/ts-command-line/src/test/ActionlessParser.test.ts index dea5cdc0a64..71c0e7edd16 100644 --- a/libraries/ts-command-line/src/test/ActionlessParser.test.ts +++ b/libraries/ts-command-line/src/test/ActionlessParser.test.ts @@ -6,6 +6,7 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter class TestCommandLine extends CommandLineParser { public flag!: CommandLineFlagParameter; + public done: boolean = false; public constructor() { super({ @@ -14,6 +15,11 @@ class TestCommandLine extends CommandLineParser { }); } + protected async onExecute(): Promise { + await super.onExecute(); + this.done = true; + } + protected onDefineParameters(): void { this.flag = this.defineFlagParameter({ parameterLongName: '--flag', @@ -23,11 +29,22 @@ class TestCommandLine extends CommandLineParser { } describe(`Actionless ${CommandLineParser.name}`, () => { + it('parses an empty arg list', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + + await commandLineParser.execute([]); + + expect(commandLineParser.done).toBe(true); + expect(commandLineParser.selectedAction).toBeUndefined(); + expect(commandLineParser.flag.value).toBe(false); + }); + it('parses a flag', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); await commandLineParser.execute(['--flag']); + expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); }); @@ -41,6 +58,7 @@ describe(`Actionless ${CommandLineParser.name}`, () => { await commandLineParser.execute(['--flag', 'the', 'remaining', 'args']); + expect(commandLineParser.done).toBe(true); expect(commandLineParser.selectedAction).toBeUndefined(); expect(commandLineParser.flag.value).toBe(true); expect(commandLineParser.remainder!.values).toEqual(['the', 'remaining', 'args']); diff --git a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts index 08c104e9d90..af389928f9a 100644 --- a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts @@ -361,31 +361,34 @@ describe(CommandLineParameter.name, () => { return commandLineParser; } - it('raises an error if env var value is not valid json', () => { + it('raises an error if env var value is not valid json', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[u'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is json containing non-scalars', () => { + it('raises an error if env var value is json containing non-scalars', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[{}]'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); - it('raises an error if env var value is not a valid choice', () => { + it('raises an error if env var value is not a valid choice', async () => { const commandLineParser: CommandLineParser = createHelloWorldParser(); const args: string[] = ['hello-world']; process.env.ENV_COLOR = 'oblong'; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect( + commandLineParser.executeWithoutErrorHandling(args) + ).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts index 62bd8cdc949..0becb8dede7 100644 --- a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts @@ -73,4 +73,25 @@ describe(CommandLineRemainder.name, () => { expect(copiedArgs).toMatchSnapshot(); }); + + it('parses an action input with remainder flagged options', async () => { + const commandLineParser: CommandLineParser = createParser(); + const action: CommandLineAction = commandLineParser.getAction('run'); + const args: string[] = ['run', '--title', 'The title', '--', '--the', 'remaining', '--args']; + + await commandLineParser.execute(args); + + expect(commandLineParser.selectedAction).toBe(action); + + const copiedArgs: string[] = []; + for (const parameter of action.parameters) { + copiedArgs.push(`### ${parameter.longName} output: ###`); + parameter.appendToArgList(copiedArgs); + } + + copiedArgs.push(`### remainder output: ###`); + action.remainder!.appendToArgList(copiedArgs); + + expect(copiedArgs).toMatchSnapshot(); + }); }); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts new file mode 100644 index 00000000000..05336946ef4 --- /dev/null +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as colors from 'colors'; + +import { ScopedCommandLineAction } from '../providers/ScopedCommandLineAction'; +import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter'; +import { CommandLineParser } from '../providers/CommandLineParser'; +import { CommandLineParameterProvider } from '../providers/CommandLineParameterProvider'; +import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter'; + +class TestScopedAction extends ScopedCommandLineAction { + public done: boolean = false; + public scopedValue: string | undefined; + private _verboseArg!: CommandLineFlagParameter; + private _scopeArg!: CommandLineStringParameter; + private _scopedArg: CommandLineStringParameter | undefined; + + public constructor() { + super({ + actionName: 'scoped-action', + summary: 'does the scoped action', + documentation: 'a longer description' + }); + } + + protected async onExecute(): Promise { + if (this._scopedArg) { + expect(this._scopedArg.longName).toBe(`--scoped-${this._scopeArg.value}`); + this.scopedValue = this._scopedArg.value; + } + this.done = true; + } + + protected onDefineUnscopedParameters(): void { + this._verboseArg = this.defineFlagParameter({ + parameterLongName: '--verbose', + description: 'A flag parameter.' + }); + + this._scopeArg = this.defineStringParameter({ + parameterLongName: '--scope', + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup, + argumentName: 'SCOPE', + description: 'The scope' + }); + } + + protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { + if (this._scopeArg.value) { + this._scopedArg = scopedParameterProvider.defineStringParameter({ + parameterLongName: `--scoped-${this._scopeArg.value}`, + argumentName: 'SCOPED', + description: 'The scoped argument.' + }); + } + } +} + +class TestCommandLine extends CommandLineParser { + public constructor() { + super({ + toolFilename: 'example', + toolDescription: 'An example project' + }); + + this.addAction(new TestScopedAction()); + } + + protected onDefineParameters(): void { + // no parameters + } +} + +describe(CommandLineParser.name, () => { + it('throws on unknown scoped arg', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; + + await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('throws on missing positional arg divider with unknown positional args', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', 'bar']; + + await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + it('executes a scoped action', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + + expect(commandLineParser.selectedAction).toBeDefined(); + expect(commandLineParser.selectedAction!.actionName).toEqual('scoped-action'); + + const action: TestScopedAction = commandLineParser.selectedAction as TestScopedAction; + expect(action.done).toBe(true); + expect(action.scopedValue).toBe('bar'); + }); + + it('prints the action help', () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const helpText: string = colors.stripColors( + commandLineParser.getAction('scoped-action').renderHelpText() + ); + expect(helpText).toMatchSnapshot(); + }); + + it('prints the scoped action help', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action to populate the help text. + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + const scopedAction: TestScopedAction & { _getScopedCommandLineParser(): CommandLineParser } = + commandLineParser.getAction('scoped-action') as TestScopedAction & { + _getScopedCommandLineParser(): CommandLineParser; + }; + const scopedCommandLineParser: CommandLineParser = scopedAction._getScopedCommandLineParser(); + const helpText: string = colors.stripColors(scopedCommandLineParser.renderHelpText()); + expect(helpText).toMatchSnapshot(); + }); + + it('prints the unscoped action parameter map', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--verbose']); + const scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(2); + const parameterStringMap: Record = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + }); + + it('prints the scoped action parameter map', async () => { + let commandLineParser: TestCommandLine = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo']); + let scopedAction: TestScopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(3); + let parameterStringMap: Record = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + + commandLineParser = new TestCommandLine(); + // Execute the parser in order to populate the scoped action + await commandLineParser.execute(['scoped-action', '--scope', 'foo', '--', '--scoped-foo', 'bar']); + scopedAction = commandLineParser.getAction('scoped-action') as TestScopedAction; + expect(scopedAction.done).toBe(true); + expect(scopedAction.parameters.length).toBe(3); + parameterStringMap = scopedAction.getParameterStringMap(); + expect(parameterStringMap).toMatchSnapshot(); + }); +}); diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap index e11dc7a569d..1250aa3605e 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap @@ -12,6 +12,19 @@ Array [ ] `; +exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` +Array [ + "### --title output: ###", + "--title", + "The title", + "### remainder output: ###", + "--", + "--the", + "remaining", + "--args", +] +`; + exports[`CommandLineRemainder prints the action help 1`] = ` "usage: example run [-h] [--title TEXT] ... diff --git a/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap new file mode 100644 index 00000000000..ca696e05d3d --- /dev/null +++ b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommandLineParser prints the action help 1`] = ` +"usage: example scoped-action [-h] [--verbose] [--scope SCOPE] ... + +a longer description + +Positional arguments: + \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- + --scopedParameter foo --scopedFlag\\". For more information on + available scoped parameters, use \\"-- --help\\". + +Optional arguments: + -h, --help Show this help message and exit. + --verbose A flag parameter. + +Optional scoping arguments: + --scope SCOPE The scope +" +`; + +exports[`CommandLineParser prints the scoped action help 1`] = ` +"usage: example scoped-action --scope foo -- [-h] [--scoped-foo SCOPED] + +a longer description + +Optional arguments: + -h, --help Show this help message and exit. + --scoped-foo SCOPED The scoped argument. + +For more information on available unscoped parameters, use \\"example +scoped-action --help\\" +" +`; + +exports[`CommandLineParser prints the scoped action parameter map 1`] = ` +Object { + "--scope": "\\"foo\\"", + "--scoped-foo": undefined, + "--verbose": "false", +} +`; + +exports[`CommandLineParser prints the scoped action parameter map 2`] = ` +Object { + "--scope": "\\"foo\\"", + "--scoped-foo": "\\"bar\\"", + "--verbose": "false", +} +`; + +exports[`CommandLineParser prints the unscoped action parameter map 1`] = ` +Object { + "--scope": undefined, + "--verbose": "true", +} +`; + +exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = `"example scoped-action: error: Unrecognized arguments: bar."`; + +exports[`CommandLineParser throws on unknown scoped arg 1`] = ` +"example scoped-action --scope foo --: error: Unrecognized arguments: --scoped-bar baz. +" +`; From 426e39872ff8e7dc4e31c28c80982625225ccd56 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Wed, 18 May 2022 14:14:18 -0700 Subject: [PATCH 08/73] Initial version of multi-stage rewrite --- apps/heft/src/cli/HeftCommandLine.ts | 317 ----- apps/heft/src/cli/HeftCommandLineParser.ts | 183 +-- apps/heft/src/cli/actions/BuildAction.ts | 85 -- apps/heft/src/cli/actions/CleanAction.ts | 44 - apps/heft/src/cli/actions/CustomAction.ts | 164 --- apps/heft/src/cli/actions/HeftActionBase.ts | 192 --- apps/heft/src/cli/actions/IHeftAction.ts | 26 + apps/heft/src/cli/actions/PhaseAction.ts | 117 ++ apps/heft/src/cli/actions/RunAction.ts | 168 +++ apps/heft/src/cli/actions/StartAction.ts | 66 - apps/heft/src/cli/actions/TestAction.ts | 93 -- .../heft/src/cli/test/CommandLineHelp.test.ts | 48 - .../CommandLineHelp.test.ts.snap | 116 -- .../src/configuration/HeftConfiguration.ts | 20 - .../configuration/HeftPluginConfiguration.ts | 196 +++ .../src/configuration/HeftPluginDefinition.ts | 371 +++++ apps/heft/src/index.ts | 85 +- apps/heft/src/metrics/MetricsCollector.ts | 65 +- .../src/operations/AsyncOperationQueue.ts | 218 +++ apps/heft/src/operations/IOperationRunner.ts | 46 + apps/heft/src/operations/Operation.ts | 68 + apps/heft/src/operations/OperationError.ts | 23 + .../operations/OperationExecutionManager.ts | 235 ++++ .../operations/OperationExecutionRecord.ts | 128 ++ apps/heft/src/operations/OperationStatus.ts | 33 + .../src/operations/PhaseOperationRunner.ts | 82 ++ .../src/operations/TaskOperationRunner.ts | 60 + .../pluginFramework/HeftEventPluginBase.ts | 173 --- .../heft/src/pluginFramework/HeftLifecycle.ts | 185 ++- .../pluginFramework/HeftLifecycleSession.ts | 101 ++ apps/heft/src/pluginFramework/HeftPhase.ts | 99 ++ .../src/pluginFramework/HeftPhaseSession.ts | 113 ++ .../src/pluginFramework/HeftPluginHost.ts | 86 ++ apps/heft/src/pluginFramework/HeftSession.ts | 121 -- apps/heft/src/pluginFramework/HeftTask.ts | 179 +++ .../src/pluginFramework/HeftTaskSession.ts | 110 ++ apps/heft/src/pluginFramework/IHeftPlugin.ts | 28 +- .../pluginFramework/InternalHeftSession.ts | 149 +- .../heft/src/pluginFramework/PluginManager.ts | 177 --- .../pluginFramework/logging/LoggingManager.ts | 12 +- .../pluginFramework/logging/ScopedLogger.ts | 11 +- .../ApiExtractorPlugin/ApiExtractorPlugin.ts | 129 -- .../ApiExtractorPlugin/ApiExtractorRunner.ts | 173 --- apps/heft/src/plugins/CopyFilesPlugin.ts | 378 +++-- .../src/plugins/CopyStaticAssetsPlugin.ts | 149 -- apps/heft/src/plugins/DeleteGlobsPlugin.ts | 106 +- apps/heft/src/plugins/NodeServicePlugin.ts | 442 ------ .../src/plugins/ProjectValidatorPlugin.ts | 172 --- apps/heft/src/plugins/RunScriptPlugin.ts | 160 +-- .../EmitCompletedCallbackManager.ts | 37 - .../TypeScriptPlugin/EmitFilesPatch.ts | 204 --- .../src/plugins/TypeScriptPlugin/Eslint.ts | 189 --- .../plugins/TypeScriptPlugin/LinterBase.ts | 201 --- .../src/plugins/TypeScriptPlugin/Tslint.ts | 190 --- .../TypeScriptPlugin/TypeScriptBuilder.ts | 1236 ----------------- .../TypeScriptPlugin/TypeScriptPlugin.ts | 270 ---- .../internalTypings/TslintInternals.ts | 23 - .../internalTypings/TypeScriptInternals.ts | 161 --- .../schemas/api-extractor-task.schema.json | 25 - .../schemas/copy-files-options.schema.json | 83 ++ .../schemas/delete-globs-options.schema.json | 21 + apps/heft/src/schemas/heft-plugin.schema.json | 249 ++++ apps/heft/src/schemas/heft.schema.json | 275 ++-- .../heft/src/schemas/node-service.schema.json | 45 - .../schemas/run-script-options.schema.json | 37 + .../schemas/templates/api-extractor-task.json | 27 - .../src/schemas/templates/node-service.json | 56 - .../src/schemas/templates/typescript.json | 85 -- apps/heft/src/schemas/typescript.schema.json | 111 -- apps/heft/src/stages/BuildStage.ts | 340 ----- apps/heft/src/stages/CleanStage.ts | 61 - apps/heft/src/stages/StageBase.ts | 94 -- apps/heft/src/stages/TestStage.ts | 48 - apps/heft/src/start.ts | 2 +- apps/heft/src/startWithVersionSelector.ts | 5 +- apps/heft/src/utilities/Constants.ts | 6 +- apps/heft/src/utilities/CoreConfigFiles.ts | 395 ++---- .../heft/src/utilities/HeftActionUtilities.ts | 214 +++ apps/heft/src/utilities/Logging.ts | 37 - apps/heft/src/utilities/Selection.ts | 123 ++ apps/heft/src/utilities/Stopwatch.ts | 116 ++ .../heft/src/utilities/ToolPackageResolver.ts | 161 --- .../fileSystem/TypeScriptCachedFileSystem.ts | 220 --- .../subprocess/SubprocessCommunication.ts | 35 - .../SubprocessCommunicationManagerBase.ts | 41 - .../subprocess/SubprocessLoggerManager.ts | 210 --- .../subprocess/SubprocessRunnerBase.ts | 424 ------ .../subprocess/SubprocessTerminalProvider.ts | 32 - .../subprocess/SubprocessTerminator.ts | 222 --- .../subprocess/TerminalProviderManager.ts | 79 -- .../utilities/subprocess/startSubprocess.ts | 51 - .../test/SubprocessRunnerBase.test.ts | 52 - .../SubprocessRunnerBase.test.ts.snap | 83 -- common/reviews/api/heft.api.md | 381 +---- common/reviews/api/node-core-library.api.md | 20 + .../node-core-library/src}/FileError.ts | 21 +- libraries/node-core-library/src/index.ts | 1 + .../src}/test/FileError.test.ts | 0 98 files changed, 4321 insertions(+), 9180 deletions(-) delete mode 100644 apps/heft/src/cli/HeftCommandLine.ts delete mode 100644 apps/heft/src/cli/actions/BuildAction.ts delete mode 100644 apps/heft/src/cli/actions/CleanAction.ts delete mode 100644 apps/heft/src/cli/actions/CustomAction.ts delete mode 100644 apps/heft/src/cli/actions/HeftActionBase.ts create mode 100644 apps/heft/src/cli/actions/IHeftAction.ts create mode 100644 apps/heft/src/cli/actions/PhaseAction.ts create mode 100644 apps/heft/src/cli/actions/RunAction.ts delete mode 100644 apps/heft/src/cli/actions/StartAction.ts delete mode 100644 apps/heft/src/cli/actions/TestAction.ts delete mode 100644 apps/heft/src/cli/test/CommandLineHelp.test.ts delete mode 100644 apps/heft/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap create mode 100644 apps/heft/src/configuration/HeftPluginConfiguration.ts create mode 100644 apps/heft/src/configuration/HeftPluginDefinition.ts create mode 100644 apps/heft/src/operations/AsyncOperationQueue.ts create mode 100644 apps/heft/src/operations/IOperationRunner.ts create mode 100644 apps/heft/src/operations/Operation.ts create mode 100644 apps/heft/src/operations/OperationError.ts create mode 100644 apps/heft/src/operations/OperationExecutionManager.ts create mode 100644 apps/heft/src/operations/OperationExecutionRecord.ts create mode 100644 apps/heft/src/operations/OperationStatus.ts create mode 100644 apps/heft/src/operations/PhaseOperationRunner.ts create mode 100644 apps/heft/src/operations/TaskOperationRunner.ts delete mode 100644 apps/heft/src/pluginFramework/HeftEventPluginBase.ts create mode 100644 apps/heft/src/pluginFramework/HeftLifecycleSession.ts create mode 100644 apps/heft/src/pluginFramework/HeftPhase.ts create mode 100644 apps/heft/src/pluginFramework/HeftPhaseSession.ts create mode 100644 apps/heft/src/pluginFramework/HeftPluginHost.ts delete mode 100644 apps/heft/src/pluginFramework/HeftSession.ts create mode 100644 apps/heft/src/pluginFramework/HeftTask.ts create mode 100644 apps/heft/src/pluginFramework/HeftTaskSession.ts delete mode 100644 apps/heft/src/pluginFramework/PluginManager.ts delete mode 100644 apps/heft/src/plugins/ApiExtractorPlugin/ApiExtractorPlugin.ts delete mode 100644 apps/heft/src/plugins/ApiExtractorPlugin/ApiExtractorRunner.ts delete mode 100644 apps/heft/src/plugins/CopyStaticAssetsPlugin.ts delete mode 100644 apps/heft/src/plugins/NodeServicePlugin.ts delete mode 100644 apps/heft/src/plugins/ProjectValidatorPlugin.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/EmitCompletedCallbackManager.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/EmitFilesPatch.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/Eslint.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/LinterBase.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/Tslint.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/TypeScriptBuilder.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/TypeScriptPlugin.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/internalTypings/TslintInternals.ts delete mode 100644 apps/heft/src/plugins/TypeScriptPlugin/internalTypings/TypeScriptInternals.ts delete mode 100644 apps/heft/src/schemas/api-extractor-task.schema.json create mode 100644 apps/heft/src/schemas/copy-files-options.schema.json create mode 100644 apps/heft/src/schemas/delete-globs-options.schema.json create mode 100644 apps/heft/src/schemas/heft-plugin.schema.json delete mode 100644 apps/heft/src/schemas/node-service.schema.json create mode 100644 apps/heft/src/schemas/run-script-options.schema.json delete mode 100644 apps/heft/src/schemas/templates/api-extractor-task.json delete mode 100644 apps/heft/src/schemas/templates/node-service.json delete mode 100644 apps/heft/src/schemas/templates/typescript.json delete mode 100644 apps/heft/src/schemas/typescript.schema.json delete mode 100644 apps/heft/src/stages/BuildStage.ts delete mode 100644 apps/heft/src/stages/CleanStage.ts delete mode 100644 apps/heft/src/stages/StageBase.ts delete mode 100644 apps/heft/src/stages/TestStage.ts create mode 100644 apps/heft/src/utilities/HeftActionUtilities.ts delete mode 100644 apps/heft/src/utilities/Logging.ts create mode 100644 apps/heft/src/utilities/Selection.ts create mode 100644 apps/heft/src/utilities/Stopwatch.ts delete mode 100644 apps/heft/src/utilities/ToolPackageResolver.ts delete mode 100644 apps/heft/src/utilities/fileSystem/TypeScriptCachedFileSystem.ts delete mode 100644 apps/heft/src/utilities/subprocess/SubprocessCommunication.ts delete mode 100644 apps/heft/src/utilities/subprocess/SubprocessCommunicationManagerBase.ts delete mode 100644 apps/heft/src/utilities/subprocess/SubprocessLoggerManager.ts delete mode 100644 apps/heft/src/utilities/subprocess/SubprocessRunnerBase.ts delete mode 100644 apps/heft/src/utilities/subprocess/SubprocessTerminalProvider.ts delete mode 100644 apps/heft/src/utilities/subprocess/SubprocessTerminator.ts delete mode 100644 apps/heft/src/utilities/subprocess/TerminalProviderManager.ts delete mode 100644 apps/heft/src/utilities/subprocess/startSubprocess.ts delete mode 100644 apps/heft/src/utilities/subprocess/test/SubprocessRunnerBase.test.ts delete mode 100644 apps/heft/src/utilities/subprocess/test/__snapshots__/SubprocessRunnerBase.test.ts.snap rename {apps/heft/src/pluginFramework/logging => libraries/node-core-library/src}/FileError.ts (84%) rename {apps/heft/src/pluginFramework/logging => libraries/node-core-library/src}/test/FileError.test.ts (100%) diff --git a/apps/heft/src/cli/HeftCommandLine.ts b/apps/heft/src/cli/HeftCommandLine.ts deleted file mode 100644 index 3cecbee152c..00000000000 --- a/apps/heft/src/cli/HeftCommandLine.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { - IBaseCommandLineDefinition, - ICommandLineFlagDefinition, - ICommandLineIntegerDefinition, - ICommandLineStringDefinition, - ICommandLineStringListDefinition, - ICommandLineChoiceDefinition, - ICommandLineChoiceListDefinition, - CommandLineAction, - CommandLineParser, - CommandLineFlagParameter, - CommandLineIntegerParameter, - CommandLineStringParameter, - CommandLineStringListParameter, - CommandLineChoiceParameter, - CommandLineChoiceListParameter, - CommandLineParameter -} from '@rushstack/ts-command-line'; -import { ITerminal } from '@rushstack/node-core-library'; - -/** - * @beta - * The base set of utility values provided in every object returned when registering a parameter. - */ -export interface IHeftBaseParameter { - /** - * The value specified on the command line for this parameter. - */ - readonly value?: TValue; - - /** - * If true, then the user has invoked Heft with a command line action that supports this parameter - * (as defined by the {@link IParameterAssociatedActionNames.associatedActionNames} option). - * - * @remarks - * For example, if `build` is one of the associated action names for `--my-integer-parameter`, - * then `actionAssociated` will be true if the user invokes `heft build`. - * - * To test whether the parameter was actually included (e.g. `heft build --my-integer-parameter 123`), - * verify the {@link IHeftBaseParameter.value} property is not `undefined`. - */ - readonly actionAssociated: boolean; - - /** - * The options {@link IHeftRegisterParameterOptions} used to create and register the parameter with - * a Heft command line action. - */ - readonly definition: IHeftRegisterParameterOptions; -} - -/** - * @beta - * The object returned when registering a choice type parameter. - */ -export type IHeftChoiceParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering a choiceList type parameter. - */ -export type IHeftChoiceListParameter = IHeftBaseParameter< - readonly string[], - ICommandLineChoiceListDefinition ->; - -/** - * @beta - * The object returned when registering a flag type parameter. - */ -export type IHeftFlagParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering an integer type parameter. - */ -export type IHeftIntegerParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering a string type parameter. - */ -export type IHeftStringParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering a stringList type parameter. - */ -export type IHeftStringListParameter = IHeftBaseParameter< - readonly string[], - ICommandLineStringListDefinition ->; - -/** - * @beta - * The configuration interface for associating a parameter definition with a Heft - * command line action in {@link IHeftRegisterParameterOptions}. - */ -export interface IParameterAssociatedActionNames { - /** - * A string list of one or more action names to associate the parameter with. - */ - associatedActionNames: string[]; -} - -/** - * @beta - * The options object provided to the command line parser when registering a parameter - * in addition to the action names used to associate the parameter with. - */ -export type IHeftRegisterParameterOptions = - TCommandLineDefinition & IParameterAssociatedActionNames; - -/** - * @beta - * Command line utilities provided for Heft plugin developers. - */ -export class HeftCommandLine { - private readonly _commandLineParser: CommandLineParser; - private readonly _terminal: ITerminal; - - /** - * @internal - */ - public constructor(commandLineParser: CommandLineParser, terminal: ITerminal) { - this._commandLineParser = commandLineParser; - this._terminal = terminal; - } - - /** - * Utility method used by Heft plugins to register a choice type parameter. - */ - public registerChoiceParameter( - options: IHeftRegisterParameterOptions - ): IHeftChoiceParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineChoiceParameter(options), - (parameter: CommandLineChoiceParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register a choiceList type parameter. - */ - public registerChoiceListParameter( - options: IHeftRegisterParameterOptions - ): IHeftChoiceListParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineChoiceListParameter(options), - (parameter: CommandLineChoiceListParameter) => parameter.values - ); - } - - /** - * Utility method used by Heft plugins to register a flag type parameter. - */ - public registerFlagParameter( - options: IHeftRegisterParameterOptions - ): IHeftFlagParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineFlagParameter(options), - (parameter: CommandLineFlagParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register an integer type parameter. - */ - public registerIntegerParameter( - options: IHeftRegisterParameterOptions - ): IHeftIntegerParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineIntegerParameter(options), - (parameter: CommandLineIntegerParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register a string type parameter. - */ - public registerStringParameter( - options: IHeftRegisterParameterOptions - ): IHeftStringParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineStringParameter(options), - (parameter: CommandLineStringParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register a stringList type parameter. - */ - public registerStringListParameter( - options: IHeftRegisterParameterOptions - ): IHeftStringListParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineStringListParameter(options), - (parameter: CommandLineStringListParameter) => parameter.values - ); - } - - private _registerParameter< - TCommandLineDefinition extends IBaseCommandLineDefinition, - TCommandLineParameter extends CommandLineParameter, - TValue - >( - options: IHeftRegisterParameterOptions, - defineParameterForAction: (action: CommandLineAction) => TCommandLineParameter, - getParameterValue: (parameter: TCommandLineParameter) => TValue | undefined - ): IHeftBaseParameter { - const actionParameterMap: Map = new Map(); - for (const action of this._getActions(options.associatedActionNames, options.parameterLongName)) { - this._verifyUniqueParameterName(action, options); - const parameter: TCommandLineParameter = defineParameterForAction(action); - actionParameterMap.set(action, parameter); - } - - const parameterObject: IHeftBaseParameter = Object.defineProperties( - {} as IHeftBaseParameter, - { - value: { - get: (): TValue | undefined => { - this._verifyParametersProcessed(options.parameterLongName); - if (this._commandLineParser.selectedAction) { - const parameter: TCommandLineParameter | undefined = actionParameterMap.get( - this._commandLineParser.selectedAction - ); - if (parameter) { - return getParameterValue(parameter); - } - } - - return undefined; - } - }, - - actionAssociated: { - get: (): boolean => { - if (!this._commandLineParser.selectedAction) { - throw new Error('Unable to determine the selected action prior to command line processing'); - } - if (actionParameterMap.get(this._commandLineParser.selectedAction)) { - return true; - } - return false; - } - }, - - definition: { - get: (): IHeftRegisterParameterOptions => { - return { ...options }; - } - } - } - ); - - return parameterObject; - } - - private _getActions(actionNames: string[], parameterLongName: string): CommandLineAction[] { - const actions: CommandLineAction[] = []; - for (const actionName of actionNames) { - const action: CommandLineAction | undefined = this._commandLineParser.tryGetAction(actionName); - if (action) { - if (action.parametersProcessed) { - throw new Error( - `Unable to register parameter "${parameterLongName}" for action "${action.actionName}". ` + - 'Parameters have already been processed.' - ); - } - actions.push(action); - } else { - this._terminal.writeVerboseLine( - `Unable to find action "${actionName}" while registering the "${parameterLongName}" parameter` - ); - } - } - return actions; - } - - private _verifyUniqueParameterName( - action: CommandLineAction, - options: IHeftRegisterParameterOptions - ): void { - const existingParameterLongNames: Set = new Set( - action.parameters.map((parameter) => parameter.longName) - ); - - if (existingParameterLongNames.has(options.parameterLongName)) { - throw new Error(`Attempting to register duplicate parameter long name: ${options.parameterLongName}`); - } - - if (options.parameterShortName) { - const existingParameterShortNames: Set = new Set( - action.parameters.map((parameter) => parameter.shortName) - ); - if (existingParameterShortNames.has(options.parameterShortName)) { - throw new Error( - `Attempting to register duplicate parameter short name: ${options.parameterShortName}` - ); - } - } - } - - private _verifyParametersProcessed(parameterName: string): void { - if (!this._commandLineParser.parametersProcessed) { - throw new Error( - `Unable to access parameter value for "${parameterName}" prior to command line processing` - ); - } - } -} diff --git a/apps/heft/src/cli/HeftCommandLineParser.ts b/apps/heft/src/cli/HeftCommandLineParser.ts index e9be91e0401..e9c7d503fc0 100644 --- a/apps/heft/src/cli/HeftCommandLineParser.ts +++ b/apps/heft/src/cli/HeftCommandLineParser.ts @@ -1,11 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { - CommandLineParser, - CommandLineStringListParameter, - CommandLineFlagParameter -} from '@rushstack/ts-command-line'; +import { ArgumentParser } from 'argparse'; +import { CommandLineParser, CommandLineFlagParameter } from '@rushstack/ts-command-line'; import { ITerminal, Terminal, @@ -15,26 +12,16 @@ import { Path, FileSystem } from '@rushstack/node-core-library'; -import { ArgumentParser } from 'argparse'; -import { SyncHook } from 'tapable'; import { MetricsCollector } from '../metrics/MetricsCollector'; -import { CleanAction } from './actions/CleanAction'; -import { BuildAction } from './actions/BuildAction'; -import { StartAction } from './actions/StartAction'; -import { TestAction } from './actions/TestAction'; -import { PluginManager } from '../pluginFramework/PluginManager'; import { HeftConfiguration } from '../configuration/HeftConfiguration'; -import { IHeftActionBaseOptions, IStages } from './actions/HeftActionBase'; import { InternalHeftSession } from '../pluginFramework/InternalHeftSession'; -import { CleanStage } from '../stages/CleanStage'; -import { BuildStage } from '../stages/BuildStage'; -import { TestStage } from '../stages/TestStage'; import { LoggingManager } from '../pluginFramework/logging/LoggingManager'; -import { ICustomActionOptions, CustomAction } from './actions/CustomAction'; import { Constants } from '../utilities/Constants'; -import { IHeftLifecycle, HeftLifecycleHooks } from '../pluginFramework/HeftLifecycle'; -import { HeftCommandLine } from './HeftCommandLine'; +import { PhaseAction } from './actions/PhaseAction'; +import { RunAction } from './actions/RunAction'; +import type { IHeftActionOptions } from './actions/IHeftAction'; +import { IHeftLifecycleToolStopHookOptions } from '../pluginFramework/HeftLifecycleSession'; /** * This interfaces specifies values for parameters that must be parsed before the CLI @@ -46,39 +33,35 @@ interface IPreInitializationArgumentValues { } export class HeftCommandLineParser extends CommandLineParser { + public readonly globalTerminal: ITerminal; + private _terminalProvider: ConsoleTerminalProvider; - private _terminal: ITerminal; private _loggingManager: LoggingManager; private _metricsCollector: MetricsCollector; - private _pluginManager: PluginManager; private _heftConfiguration: HeftConfiguration; - private _internalHeftSession: InternalHeftSession; - private _heftLifecycleHook: SyncHook; + private _internalHeftSession: InternalHeftSession | undefined; private _preInitializationArgumentValues: IPreInitializationArgumentValues; - private _unmanagedFlag!: CommandLineFlagParameter; private _debugFlag!: CommandLineFlagParameter; - private _pluginsParameter!: CommandLineStringListParameter; public get isDebug(): boolean { return !!this._preInitializationArgumentValues.debug; } - public get terminal(): ITerminal { - return this._terminal; - } - public constructor() { super({ toolFilename: 'heft', toolDescription: 'Heft is a pluggable build system designed for web projects.' }); + // Pre-initialize with known argument values to determine state of "--debug" this._preInitializationArgumentValues = this._getPreInitializationArgumentValues(); - this._terminalProvider = new ConsoleTerminalProvider(); - this._terminal = new Terminal(this._terminalProvider); + this._terminalProvider = new ConsoleTerminalProvider({ + debugEnabled: this.isDebug + }); + this.globalTerminal = new Terminal(this._terminalProvider); this._metricsCollector = new MetricsCollector(); this._loggingManager = new LoggingManager({ terminalProvider: this._terminalProvider @@ -93,72 +76,13 @@ export class HeftCommandLineParser extends CommandLineParser { cwd: process.cwd(), terminalProvider: this._terminalProvider }); - - const stages: IStages = { - buildStage: new BuildStage(this._heftConfiguration, this._loggingManager), - cleanStage: new CleanStage(this._heftConfiguration, this._loggingManager), - testStage: new TestStage(this._heftConfiguration, this._loggingManager) - }; - - const actionOptions: IHeftActionBaseOptions = { - terminal: this._terminal, - loggingManager: this._loggingManager, - metricsCollector: this._metricsCollector, - heftConfiguration: this._heftConfiguration, - stages - }; - - this._heftLifecycleHook = new SyncHook(['heftLifecycle']); - this._internalHeftSession = new InternalHeftSession({ - getIsDebugMode: () => this.isDebug, - ...stages, - heftLifecycleHook: this._heftLifecycleHook, - loggingManager: this._loggingManager, - metricsCollector: this._metricsCollector, - registerAction: (options: ICustomActionOptions) => { - const action: CustomAction = new CustomAction(options, actionOptions); - this.addAction(action); - }, - commandLine: new HeftCommandLine(this, this._terminal) - }); - - this._pluginManager = new PluginManager({ - terminal: this._terminal, - heftConfiguration: this._heftConfiguration, - internalHeftSession: this._internalHeftSession - }); - - const cleanAction: CleanAction = new CleanAction(actionOptions); - const buildAction: BuildAction = new BuildAction(actionOptions); - const startAction: StartAction = new StartAction(actionOptions); - const testAction: TestAction = new TestAction(actionOptions); - - this.addAction(cleanAction); - this.addAction(buildAction); - this.addAction(startAction); - this.addAction(testAction); } protected onDefineParameters(): void { - this._unmanagedFlag = this.defineFlagParameter({ - parameterLongName: '--unmanaged', - description: - 'Disables the Heft version selector: When Heft is invoked via the shell path, normally it' + - " will examine the project's package.json dependencies and try to use the locally installed version" + - ' of Heft. Specify "--unmanaged" to force the invoked version of Heft to be used. This is useful for' + - ' example if you want to test a different version of Heft.' - }); - this._debugFlag = this.defineFlagParameter({ parameterLongName: Constants.debugParameterLongName, description: 'Show the full call stack if an error occurs while executing the tool' }); - - this._pluginsParameter = this.defineStringListParameter({ - parameterLongName: Constants.pluginParameterLongName, - argumentName: 'PATH', - description: 'Used to specify Heft plugins.' - }); } public async execute(args?: string[]): Promise { @@ -181,17 +105,39 @@ export class HeftCommandLineParser extends CommandLineParser { pathToConvert: rigProfileFolder, baseFolder: this._heftConfiguration.buildFolder }); - this._terminal.writeLine(`Using rig configuration from ${relativeRigFolderPath}`); + this.globalTerminal.writeLine(`Using rig configuration from ${relativeRigFolderPath}`); } - await this._initializePluginsAsync(); + // await this._initializePluginsAsync(); + + // const heftLifecycle: IHeftLifecycle = { + // hooks: new HeftLifecycleHooks() + // }; + // this._heftLifecycleHook.call(heftLifecycle); + + // await heftLifecycle.hooks.toolStart.promise(); + + const internalHeftSession: InternalHeftSession = await InternalHeftSession.initializeAsync({ + heftConfiguration: this._heftConfiguration, + loggingManager: this._loggingManager, + metricsCollector: this._metricsCollector, + getIsDebugMode: () => this.isDebug + }); - const heftLifecycle: IHeftLifecycle = { - hooks: new HeftLifecycleHooks() + await internalHeftSession.lifecycle.ensureInitializedAsync(); + + const actionOptions: IHeftActionOptions = { + internalHeftSession: internalHeftSession, + terminal: this.globalTerminal, + loggingManager: this._loggingManager, + metricsCollector: this._metricsCollector, + heftConfiguration: this._heftConfiguration }; - this._heftLifecycleHook.call(heftLifecycle); - await heftLifecycle.hooks.toolStart.promise(); + this.addAction(new RunAction(actionOptions)); + for (const phase of internalHeftSession.phases) { + this.addAction(new PhaseAction({ phase, ...actionOptions })); + } return await super.execute(args); } catch (e) { @@ -204,10 +150,10 @@ export class HeftCommandLineParser extends CommandLineParser { // The .heft/clean.json file is a fairly reliable heuristic for detecting projects created prior to // the big config file redesign with Heft 0.14.0 if (await FileSystem.existsAsync('.heft/clean.json')) { - this._terminal.writeErrorLine( + this.globalTerminal.writeErrorLine( '\nThis project has a ".heft/clean.json" file, which is now obsolete as of Heft 0.14.0.' ); - this._terminal.writeLine( + this.globalTerminal.writeLine( '\nFor instructions for migrating config files, please read UPGRADING.md in the @rushstack/heft package folder.\n' ); throw new AlreadyReportedError(); @@ -217,7 +163,10 @@ export class HeftCommandLineParser extends CommandLineParser { protected async onExecute(): Promise { try { await super.onExecute(); - await this._metricsCollector.flushAndTeardownAsync(); + if (this._internalHeftSession) { + const toolStopHookOptions: IHeftLifecycleToolStopHookOptions = {}; + await this._internalHeftSession.lifecycle.hooks.toolStop.promise(toolStopHookOptions); + } } catch (e) { await this._reportErrorAndSetExitCode(e as Error); } @@ -228,11 +177,11 @@ export class HeftCommandLineParser extends CommandLineParser { private _normalizeCwd(): void { const buildFolder: string = this._heftConfiguration.buildFolder; - this._terminal.writeLine(`Project build folder is "${buildFolder}"`); + this.globalTerminal.writeLine(`Project build folder is "${buildFolder}"`); const currentCwd: string = process.cwd(); if (currentCwd !== buildFolder) { // Update the CWD to the project's build root. Some tools, like Jest, use process.cwd() - this._terminal.writeVerboseLine(`CWD is "${currentCwd}". Normalizing to project build folder.`); + this.globalTerminal.writeVerboseLine(`CWD is "${currentCwd}". Normalizing to project build folder.`); // If `process.cwd()` and `buildFolder` differ only by casing on Windows, the chdir operation will not fix the casing, which is the entire purpose of the exercise. // As such, chdir to a different directory first. That directory needs to exist, so use the parent of the current directory. // This will not work if the current folder is the drive root, but that is a rather exotic case. @@ -244,39 +193,41 @@ export class HeftCommandLineParser extends CommandLineParser { private _getPreInitializationArgumentValues( args: string[] = process.argv ): IPreInitializationArgumentValues { - // This is a rough parsing of the --plugin parameters + // This is a rough parsing of the --debug parameter const parser: ArgumentParser = new ArgumentParser({ addHelp: false }); - parser.addArgument(this._pluginsParameter.longName, { dest: 'plugins', action: 'append' }); parser.addArgument(this._debugFlag.longName, { dest: 'debug', action: 'storeTrue' }); const [result]: IPreInitializationArgumentValues[] = parser.parseKnownArgs(args); return result; } - private async _initializePluginsAsync(): Promise { - this._pluginManager.initializeDefaultPlugins(); + // private async _initializePluginsAsync(): Promise { + // this._pluginManager.initializeDefaultPlugins(); - await this._pluginManager.initializePluginsFromConfigFileAsync(); + // await this._pluginManager.initializePluginsFromConfigFileAsync(); - const pluginSpecifiers: string[] = this._preInitializationArgumentValues.plugins || []; - for (const pluginSpecifier of pluginSpecifiers) { - this._pluginManager.initializePlugin(pluginSpecifier); - } + // const pluginSpecifiers: string[] = this._preInitializationArgumentValues.plugins || []; + // for (const pluginSpecifier of pluginSpecifiers) { + // this._pluginManager.initializePlugin(pluginSpecifier); + // } - this._pluginManager.afterInitializeAllPlugins(); - } + // this._pluginManager.afterInitializeAllPlugins(); + // } private async _reportErrorAndSetExitCode(error: Error): Promise { if (!(error instanceof AlreadyReportedError)) { - this._terminal.writeErrorLine(error.toString()); + this.globalTerminal.writeErrorLine(error.toString()); } if (this.isDebug) { - this._terminal.writeLine(); - this._terminal.writeErrorLine(error.stack!); + this.globalTerminal.writeLine(); + this.globalTerminal.writeErrorLine(error.stack!); } - await this._metricsCollector.flushAndTeardownAsync(); + if (this._internalHeftSession) { + const toolStopHookOptions: IHeftLifecycleToolStopHookOptions = {}; + await this._internalHeftSession.lifecycle.hooks.toolStop.promise(toolStopHookOptions); + } if (!process.exitCode || process.exitCode > 0) { process.exit(process.exitCode); diff --git a/apps/heft/src/cli/actions/BuildAction.ts b/apps/heft/src/cli/actions/BuildAction.ts deleted file mode 100644 index 927e71ebed5..00000000000 --- a/apps/heft/src/cli/actions/BuildAction.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { CommandLineFlagParameter, ICommandLineActionOptions } from '@rushstack/ts-command-line'; - -import { HeftActionBase, IHeftActionBaseOptions } from './HeftActionBase'; -import { CleanStage, ICleanStageOptions } from '../../stages/CleanStage'; -import { Logging } from '../../utilities/Logging'; -import { BuildStage, IBuildStageOptions, IBuildStageStandardParameters } from '../../stages/BuildStage'; - -export class BuildAction extends HeftActionBase { - protected _watchFlag!: CommandLineFlagParameter; - protected _productionFlag!: CommandLineFlagParameter; - protected _liteFlag!: CommandLineFlagParameter; - private _buildStandardParameters!: IBuildStageStandardParameters; - private _cleanFlag!: CommandLineFlagParameter; - - public constructor( - heftActionOptions: IHeftActionBaseOptions, - commandLineActionOptions: ICommandLineActionOptions = { - actionName: 'build', - summary: 'Build the project.', - documentation: '' - } - ) { - super(commandLineActionOptions, heftActionOptions); - } - - public onDefineParameters(): void { - super.onDefineParameters(); - - this._buildStandardParameters = BuildStage.defineStageStandardParameters(this); - this._productionFlag = this._buildStandardParameters.productionFlag; - this._liteFlag = this._buildStandardParameters.liteFlag; - - this._watchFlag = this.defineFlagParameter({ - parameterLongName: '--watch', - parameterShortName: '-w', - description: 'If provided, run tests in watch mode.' - }); - - this._cleanFlag = this.defineFlagParameter({ - parameterLongName: '--clean', - description: 'If specified, clean the package before building.' - }); - } - - protected async actionExecuteAsync(): Promise { - await this.runCleanIfRequestedAsync(); - await this.runBuildAsync(); - } - - protected async runCleanIfRequestedAsync(): Promise { - if (this._cleanFlag.value) { - const cleanStage: CleanStage = this.stages.cleanStage; - const cleanStageOptions: ICleanStageOptions = {}; - await cleanStage.initializeAsync(cleanStageOptions); - - await Logging.runFunctionWithLoggingBoundsAsync( - this.terminal, - 'Clean', - async () => await cleanStage.executeAsync() - ); - } - } - - protected async runBuildAsync(): Promise { - const buildStage: BuildStage = this.stages.buildStage; - const buildStageOptions: IBuildStageOptions = { - ...BuildStage.getOptionsFromStandardParameters(this._buildStandardParameters), - watchMode: this._watchFlag.value, - serveMode: false - }; - await buildStage.initializeAsync(buildStageOptions); - await buildStage.executeAsync(); - } - - protected async afterExecuteAsync(): Promise { - if (this._watchFlag.value) { - await new Promise(() => { - /* never continue if in --watch mode */ - }); - } - } -} diff --git a/apps/heft/src/cli/actions/CleanAction.ts b/apps/heft/src/cli/actions/CleanAction.ts deleted file mode 100644 index e72c1920299..00000000000 --- a/apps/heft/src/cli/actions/CleanAction.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { CommandLineFlagParameter } from '@rushstack/ts-command-line'; - -import { HeftActionBase, IHeftActionBaseOptions } from './HeftActionBase'; -import { CleanStage, ICleanStageOptions } from '../../stages/CleanStage'; - -export class CleanAction extends HeftActionBase { - private _deleteCacheFlag!: CommandLineFlagParameter; - - public constructor(options: IHeftActionBaseOptions) { - super( - { - actionName: 'clean', - summary: 'Clean the project', - documentation: '' - }, - options - ); - } - - public onDefineParameters(): void { - super.onDefineParameters(); - - this._deleteCacheFlag = this.defineFlagParameter({ - parameterLongName: '--clear-cache', - description: - "If this flag is provided, the compiler cache will also be cleared. This isn't dangerous, " + - 'but may lead to longer compile times' - }); - } - - protected async actionExecuteAsync(): Promise { - const cleanStage: CleanStage = this.stages.cleanStage; - - const cleanStageOptions: ICleanStageOptions = { - deleteCache: this._deleteCacheFlag.value - }; - await cleanStage.initializeAsync(cleanStageOptions); - - await cleanStage.executeAsync(); - } -} diff --git a/apps/heft/src/cli/actions/CustomAction.ts b/apps/heft/src/cli/actions/CustomAction.ts deleted file mode 100644 index 81f762c875e..00000000000 --- a/apps/heft/src/cli/actions/CustomAction.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { - CommandLineFlagParameter, - CommandLineStringParameter, - CommandLineIntegerParameter, - CommandLineStringListParameter -} from '@rushstack/ts-command-line'; - -import { HeftActionBase, IHeftActionBaseOptions } from './HeftActionBase'; - -/** @beta */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export interface ICustomActionParameterBase { - kind: 'flag' | 'integer' | 'string' | 'stringList'; // TODO: Add "choice" - - parameterLongName: string; - description: string; -} - -/** @beta */ -export interface ICustomActionParameterFlag extends ICustomActionParameterBase { - kind: 'flag'; -} - -/** @beta */ -export interface ICustomActionParameterInteger extends ICustomActionParameterBase { - kind: 'integer'; -} - -/** @beta */ -export interface ICustomActionParameterString extends ICustomActionParameterBase { - kind: 'string'; -} - -/** @beta */ -export interface ICustomActionParameterStringList extends ICustomActionParameterBase> { - kind: 'stringList'; -} - -/** @beta */ -export type CustomActionParameterType = string | boolean | number | ReadonlyArray | undefined; - -/** @beta */ -export type ICustomActionParameter = TParameter extends boolean - ? ICustomActionParameterFlag - : TParameter extends number - ? ICustomActionParameterInteger - : TParameter extends string - ? ICustomActionParameterString - : TParameter extends ReadonlyArray - ? ICustomActionParameterStringList - : never; - -/** @beta */ -export interface ICustomActionOptions { - actionName: string; - documentation: string; - summary?: string; - - parameters?: { [K in keyof TParameters]: ICustomActionParameter }; - - callback: (parameters: TParameters) => void | Promise; -} - -export class CustomAction extends HeftActionBase { - private _customActionOptions: ICustomActionOptions; - private _parameterValues!: Map CustomActionParameterType>; - - public constructor( - customActionOptions: ICustomActionOptions, - options: IHeftActionBaseOptions - ) { - super( - { - actionName: customActionOptions.actionName, - documentation: customActionOptions.documentation, - summary: customActionOptions.summary || '' - }, - options - ); - - this._customActionOptions = customActionOptions; - } - - public onDefineParameters(): void { - super.onDefineParameters(); - - this._parameterValues = new Map CustomActionParameterType>(); - for (const [callbackValueName, untypedParameterOption] of Object.entries( - this._customActionOptions.parameters || {} - )) { - if (this._parameterValues.has(callbackValueName)) { - throw new Error(`Duplicate callbackValueName: ${callbackValueName}`); - } - - let getParameterValue: () => CustomActionParameterType; - - const parameterOption: ICustomActionParameter = - untypedParameterOption as ICustomActionParameter; - switch (parameterOption.kind) { - case 'flag': { - const parameter: CommandLineFlagParameter = this.defineFlagParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description - }); - getParameterValue = () => parameter.value; - break; - } - - case 'string': { - const parameter: CommandLineStringParameter = this.defineStringParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description, - argumentName: 'VALUE' - }); - getParameterValue = () => parameter.value; - break; - } - - case 'integer': { - const parameter: CommandLineIntegerParameter = this.defineIntegerParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description, - argumentName: 'VALUE' - }); - getParameterValue = () => parameter.value; - break; - } - - case 'stringList': { - const parameter: CommandLineStringListParameter = this.defineStringListParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description, - argumentName: 'VALUE' - }); - getParameterValue = () => parameter.values; - break; - } - - default: { - throw new Error( - // @ts-expect-error All cases are handled above, therefore parameterOption is of type `never` - `Unrecognized parameter kind "${parameterOption.kind}" for parameter "${parameterOption.parameterLongName}` - ); - } - } - - this._parameterValues.set(callbackValueName, getParameterValue); - } - } - - protected async actionExecuteAsync(): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const parameterValues: Record = {}; - - for (const [callbackName, getParameterValue] of this._parameterValues.entries()) { - parameterValues[callbackName] = getParameterValue(); - } - - await this._customActionOptions.callback(parameterValues as TParameters); - } -} diff --git a/apps/heft/src/cli/actions/HeftActionBase.ts b/apps/heft/src/cli/actions/HeftActionBase.ts deleted file mode 100644 index 76fee89ffa6..00000000000 --- a/apps/heft/src/cli/actions/HeftActionBase.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { - CommandLineAction, - CommandLineFlagParameter, - ICommandLineActionOptions, - ICommandLineFlagDefinition, - IBaseCommandLineDefinition, - ICommandLineChoiceDefinition, - CommandLineChoiceParameter, - CommandLineIntegerParameter, - ICommandLineIntegerDefinition, - CommandLineStringParameter, - ICommandLineStringDefinition, - CommandLineStringListParameter, - ICommandLineStringListDefinition -} from '@rushstack/ts-command-line'; -import { - ITerminal, - IPackageJson, - Colors, - ConsoleTerminalProvider, - AlreadyReportedError -} from '@rushstack/node-core-library'; -import { performance } from 'perf_hooks'; - -import { IPerformanceData, MetricsCollector } from '../../metrics/MetricsCollector'; -import { HeftConfiguration } from '../../configuration/HeftConfiguration'; -import { BuildStage } from '../../stages/BuildStage'; -import { CleanStage } from '../../stages/CleanStage'; -import { TestStage } from '../../stages/TestStage'; -import { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; -import { Constants } from '../../utilities/Constants'; - -export interface IStages { - buildStage: BuildStage; - cleanStage: CleanStage; - testStage: TestStage; -} - -export interface IHeftActionBaseOptions { - terminal: ITerminal; - loggingManager: LoggingManager; - metricsCollector: MetricsCollector; - heftConfiguration: HeftConfiguration; - stages: IStages; -} - -export abstract class HeftActionBase extends CommandLineAction { - protected readonly terminal: ITerminal; - protected readonly loggingManager: LoggingManager; - protected readonly metricsCollector: MetricsCollector; - protected readonly heftConfiguration: HeftConfiguration; - protected readonly stages: IStages; - protected verboseFlag!: CommandLineFlagParameter; - - public constructor( - commandLineOptions: ICommandLineActionOptions, - heftActionOptions: IHeftActionBaseOptions - ) { - super(commandLineOptions); - this.terminal = heftActionOptions.terminal; - this.loggingManager = heftActionOptions.loggingManager; - this.metricsCollector = heftActionOptions.metricsCollector; - this.heftConfiguration = heftActionOptions.heftConfiguration; - this.stages = heftActionOptions.stages; - this.setStartTime(); - } - - public onDefineParameters(): void { - this.verboseFlag = this.defineFlagParameter({ - parameterLongName: '--verbose', - parameterShortName: '-v', - description: 'If specified, log information useful for debugging.' - }); - } - - public defineChoiceParameter(options: ICommandLineChoiceDefinition): CommandLineChoiceParameter { - this._validateDefinedParameter(options); - return super.defineChoiceParameter(options); - } - - public defineFlagParameter(options: ICommandLineFlagDefinition): CommandLineFlagParameter { - this._validateDefinedParameter(options); - return super.defineFlagParameter(options); - } - - public defineIntegerParameter(options: ICommandLineIntegerDefinition): CommandLineIntegerParameter { - this._validateDefinedParameter(options); - return super.defineIntegerParameter(options); - } - - public defineStringParameter(options: ICommandLineStringDefinition): CommandLineStringParameter { - this._validateDefinedParameter(options); - return super.defineStringParameter(options); - } - - public defineStringListParameter( - options: ICommandLineStringListDefinition - ): CommandLineStringListParameter { - this._validateDefinedParameter(options); - return super.defineStringListParameter(options); - } - - public setStartTime(): void { - this.metricsCollector.setStartTime(); - } - - public recordMetrics(performanceData?: Partial): void { - this.metricsCollector.record(this.actionName, performanceData, this.getParameterStringMap()); - } - - public async onExecute(): Promise { - this.terminal.writeLine(`Starting ${this.actionName}`); - - if (this.verboseFlag.value) { - if (this.heftConfiguration.terminalProvider instanceof ConsoleTerminalProvider) { - this.heftConfiguration.terminalProvider.verboseEnabled = true; - } - } - - let encounteredError: boolean = false; - try { - await this.actionExecuteAsync(); - await this.afterExecuteAsync(); - } catch (e) { - encounteredError = true; - throw e; - } finally { - const warningStrings: string[] = this.loggingManager.getWarningStrings(); - const errorStrings: string[] = this.loggingManager.getErrorStrings(); - - const encounteredWarnings: boolean = warningStrings.length > 0; - encounteredError = encounteredError || errorStrings.length > 0; - - this.recordMetrics({ encounteredError }); - - this.terminal.writeLine( - Colors.bold( - (encounteredError ? Colors.red : encounteredWarnings ? Colors.yellow : Colors.green)( - `-------------------- Finished (${Math.round(performance.now()) / 1000}s) --------------------` - ) - ) - ); - - if (warningStrings.length > 0) { - this.terminal.writeWarningLine(`Encountered ${warningStrings.length} warnings:`); - for (const warningString of warningStrings) { - this.terminal.writeWarningLine(` ${warningString}`); - } - } - - if (errorStrings.length > 0) { - this.terminal.writeErrorLine(`Encountered ${errorStrings.length} errors:`); - for (const errorString of errorStrings) { - this.terminal.writeErrorLine(` ${errorString}`); - } - } - - const projectPackageJson: IPackageJson = this.heftConfiguration.projectPackageJson; - this.terminal.writeLine( - `Project: ${projectPackageJson.name}`, - Colors.dim(Colors.gray(`@${projectPackageJson.version}`)) - ); - this.terminal.writeLine(`Heft version: ${this.heftConfiguration.heftPackageJson.version}`); - this.terminal.writeLine(`Node version: ${process.version}`); - } - - if (encounteredError) { - throw new AlreadyReportedError(); - } - } - - protected abstract actionExecuteAsync(): Promise; - - /** - * @virtual - */ - protected async afterExecuteAsync(): Promise { - /* no-op by default */ - } - - private _validateDefinedParameter(options: IBaseCommandLineDefinition): void { - if ( - options.parameterLongName === Constants.pluginParameterLongName || - options.parameterLongName === Constants.debugParameterLongName - ) { - throw new Error(`Actions must not register a parameter with longName "${options.parameterLongName}".`); - } - } -} diff --git a/apps/heft/src/cli/actions/IHeftAction.ts b/apps/heft/src/cli/actions/IHeftAction.ts new file mode 100644 index 00000000000..6e401930346 --- /dev/null +++ b/apps/heft/src/cli/actions/IHeftAction.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { CommandLineAction } from '@rushstack/ts-command-line'; +import type { ITerminal } from '@rushstack/node-core-library'; + +import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; +import type { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; +import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; + +export interface IHeftActionOptions { + internalHeftSession: InternalHeftSession; + terminal: ITerminal; + loggingManager: LoggingManager; + metricsCollector: MetricsCollector; + heftConfiguration: HeftConfiguration; +} + +export interface IHeftAction extends CommandLineAction { + readonly terminal: ITerminal; + readonly loggingManager: LoggingManager; + readonly metricsCollector: MetricsCollector; + readonly heftConfiguration: HeftConfiguration; + readonly verbose: boolean; +} diff --git a/apps/heft/src/cli/actions/PhaseAction.ts b/apps/heft/src/cli/actions/PhaseAction.ts new file mode 100644 index 00000000000..7c0f00cd93b --- /dev/null +++ b/apps/heft/src/cli/actions/PhaseAction.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { CommandLineAction, CommandLineFlagParameter } from '@rushstack/ts-command-line'; +import type { ITerminal } from '@rushstack/node-core-library'; + +import { + createOperations, + initializeAction, + executeInstrumentedAsync +} from '../../utilities/HeftActionUtilities'; +import { + IOperationExecutionManagerOptions, + OperationExecutionManager +} from '../../operations/OperationExecutionManager'; +import { Operation } from '../../operations/Operation'; +import { Selection } from '../../utilities/Selection'; +import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; +import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import type { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; +import type { IHeftAction, IHeftActionOptions } from './IHeftAction'; +import type { HeftPhase } from '../../pluginFramework/HeftPhase'; + +export interface IPhaseActionOptions extends IHeftActionOptions { + phase: HeftPhase; +} + +export class PhaseAction extends CommandLineAction implements IHeftAction { + public readonly terminal: ITerminal; + public readonly loggingManager: LoggingManager; + public readonly metricsCollector: MetricsCollector; + public readonly heftConfiguration: HeftConfiguration; + + private _verboseFlag!: CommandLineFlagParameter; + private _productionFlag!: CommandLineFlagParameter; + private _cleanFlag!: CommandLineFlagParameter; + + private _internalSession: InternalHeftSession; + private _selectedPhases: Set; + + public get verbose(): boolean { + return this._verboseFlag.value; + } + + public constructor(options: IPhaseActionOptions) { + super({ + actionName: options.phase.phaseName, + documentation: + `Runs to the ${options.phase.phaseName} phase, including all transitive dependencies.` + + (options.phase.phaseDescription ? ` ${options.phase.phaseDescription}` : ''), + summary: `Runs to the ${options.phase.phaseName} phase, including all transitive dependencies.` + }); + + this.terminal = options.terminal; + this.loggingManager = options.loggingManager; + this.metricsCollector = options.metricsCollector; + this.heftConfiguration = options.heftConfiguration; + + this._internalSession = options.internalHeftSession; + + this._selectedPhases = Selection.expandAllDependencies( + [options.phase], + (phase: HeftPhase) => phase.dependencyPhases + ); + + initializeAction(this); + } + + public onDefineParameters(): void { + this._verboseFlag = this.defineFlagParameter({ + parameterLongName: '--verbose', + parameterShortName: '-v', + description: 'If specified, log information useful for debugging.' + }); + this._productionFlag = this.defineFlagParameter({ + parameterLongName: '--production', + description: 'If specified, run Heft in production mode.' + }); + this._cleanFlag = this.defineFlagParameter({ + parameterLongName: '--clean', + description: 'If specified, clean the package before running.' + }); + + for (const phase of this._selectedPhases) { + for (const task of phase.tasks) { + task.pluginDefinition.defineParameters(this); + } + } + } + + protected async onExecute(): Promise { + await executeInstrumentedAsync({ + action: this, + executeAsync: async () => { + const operations: Set = createOperations({ + internalHeftSession: this._internalSession, + terminal: this.terminal, + production: this._productionFlag.value, + clean: this._cleanFlag.value + }); + const operationExecutionManagerOptions: IOperationExecutionManagerOptions = { + loggingManager: this.loggingManager, + terminal: this.terminal, + debugMode: this._internalSession.debugMode, + // TODO: Allow for running non-parallelized operations. + parallelism: undefined + }; + const executionManager: OperationExecutionManager = new OperationExecutionManager( + operations, + operationExecutionManagerOptions + ); + await executionManager.executeAsync(); + } + }); + } +} diff --git a/apps/heft/src/cli/actions/RunAction.ts b/apps/heft/src/cli/actions/RunAction.ts new file mode 100644 index 00000000000..ed2eb0b10ae --- /dev/null +++ b/apps/heft/src/cli/actions/RunAction.ts @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + CommandLineFlagParameter, + CommandLineParameterProvider, + CommandLineStringListParameter, + ScopedCommandLineAction +} from '@rushstack/ts-command-line'; +import { AlreadyReportedError, InternalError, ITerminal } from '@rushstack/node-core-library'; + +import { + createOperations, + executeInstrumentedAsync, + initializeAction +} from '../../utilities/HeftActionUtilities'; +import { Selection } from '../../utilities/Selection'; +import { + OperationExecutionManager, + type IOperationExecutionManagerOptions +} from '../../operations/OperationExecutionManager'; +import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import type { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; +import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; +import type { IHeftAction, IHeftActionOptions } from './IHeftAction'; +import type { HeftPhase } from '../../pluginFramework/HeftPhase'; +import type { Operation } from '../../operations/Operation'; + +export interface IRunActionOptions extends IHeftActionOptions {} + +export class RunAction extends ScopedCommandLineAction implements IHeftAction { + public readonly terminal: ITerminal; + public readonly loggingManager: LoggingManager; + public readonly metricsCollector: MetricsCollector; + public readonly heftConfiguration: HeftConfiguration; + + private _verboseFlag!: CommandLineFlagParameter; + private _productionFlag!: CommandLineFlagParameter; + private _cleanFlag!: CommandLineFlagParameter; + private _to!: CommandLineStringListParameter; + private _only!: CommandLineStringListParameter; + + private _internalSession: InternalHeftSession; + private _selectedPhases: Set | undefined; + + public get verbose(): boolean { + return this._verboseFlag.value; + } + + public constructor(options: IRunActionOptions) { + super({ + actionName: 'run', + documentation: 'Run a provided selection of Heft phases.', + summary: 'Run a provided selection of Heft phases.' + }); + + this.terminal = options.terminal; + this.loggingManager = options.loggingManager; + this.metricsCollector = options.metricsCollector; + this.heftConfiguration = options.heftConfiguration; + + this._internalSession = options.internalHeftSession; + + initializeAction(this); + } + + protected onDefineUnscopedParameters(): void { + this._to = this.defineStringListParameter({ + parameterLongName: '--to', + parameterShortName: '-t', + description: 'The phase to run to, including all transitive dependencies.', + argumentName: 'PHASE', + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup + }); + this._only = this.defineStringListParameter({ + parameterLongName: '--only', + parameterShortName: '-o', + description: 'The phase to run.', + argumentName: 'PHASE', + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup + }); + } + + protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { + // Define these flags here, since we want them to be available to all scoped actions. + // It also makes it easier to append these flags when using NPM scripts, for example: + // "npm run