From 6f917d351eede7297e65132dbb3f33a3622be6c3 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/16] Update argparse version --- libraries/ts-command-line/package.json | 4 +- .../src/providers/CommandLineAction.ts | 2 +- .../providers/CommandLineParameterProvider.ts | 43 +++++++------ .../src/providers/CommandLineParser.ts | 11 ++-- .../CommandLineParameter.test.ts.snap | 61 ++++++------------- .../CommandLineRemainder.test.ts.snap | 28 ++++++--- 6 files changed, 71 insertions(+), 78 deletions(-) diff --git a/libraries/ts-command-line/package.json b/libraries/ts-command-line/package.json index cab7c32bc5f..e389f405916 100644 --- a/libraries/ts-command-line/package.json +++ b/libraries/ts-command-line/package.json @@ -16,8 +16,8 @@ }, "license": "MIT", "dependencies": { - "@types/argparse": "1.0.38", - "argparse": "~1.0.9", + "@types/argparse": "2.0.10", + "argparse": "~2.0.1", "colors": "~1.2.1", "string-argv": "~0.3.1" }, diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index a0bd8deb4d9..c3e79814cbe 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -78,7 +78,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * @internal */ public _buildParser(actionsSubParser: argparse.SubParser): void { - this._argumentParser = actionsSubParser.addParser(this.actionName, { + this._argumentParser = actionsSubParser.add_parser(this.actionName, { help: this.summary, description: this.documentation }); diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 48e0818d01f..73bd82030a2 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -272,11 +272,11 @@ export abstract class CommandLineParameterProvider { const argparseOptions: argparse.ArgumentOptions = { help: this._remainder.description, - nargs: argparse.Const.REMAINDER, + nargs: argparse.REMAINDER, metavar: '"..."' }; - this._getArgumentParser().addArgument(argparse.Const.REMAINDER, argparseOptions); + this._getArgumentParser().add_argument(argparse.REMAINDER, argparseOptions); return this._remainder; } @@ -294,7 +294,7 @@ export abstract class CommandLineParameterProvider { * Generates the command-line help text. */ public renderHelpText(): string { - return this._getArgumentParser().formatHelp(); + return this._getArgumentParser().format_help(); } /** @@ -361,7 +361,7 @@ export abstract class CommandLineParameterProvider { } if (this.remainder) { - this.remainder._setValue(data[argparse.Const.REMAINDER]); + this.remainder._setValue(data[argparse.REMAINDER]); } this._parametersProcessed = true; @@ -396,12 +396,6 @@ export abstract class CommandLineParameterProvider { ); } - const names: string[] = []; - if (parameter.shortName) { - names.push(parameter.shortName); - } - names.push(parameter.longName); - parameter._parserKey = this._generateKey(); let finalDescription: string = parameter.description; @@ -422,10 +416,16 @@ export abstract class CommandLineParameterProvider { const argparseOptions: argparse.ArgumentOptions = { help: finalDescription, dest: parameter._parserKey, - metavar: (parameter as CommandLineParameterWithArgument).argumentName || undefined, required: parameter.required }; + // Only add the metavar if it's specified. Setting to undefined will cause argparse to throw when + // metavar isn't + const metavarValue: string | undefined = (parameter as CommandLineParameterWithArgument).argumentName; + if (metavarValue) { + argparseOptions.metavar = metavarValue; + } + switch (parameter.kind) { case CommandLineParameterKind.Choice: { const choiceParameter: CommandLineChoiceParameter = parameter as CommandLineChoiceParameter; @@ -439,7 +439,7 @@ export abstract class CommandLineParameterProvider { break; } case CommandLineParameterKind.Flag: - argparseOptions.action = 'storeTrue'; + argparseOptions.action = 'store_true'; break; case CommandLineParameterKind.Integer: argparseOptions.type = 'int'; @@ -456,12 +456,19 @@ export abstract class CommandLineParameterProvider { } const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); - argumentParser.addArgument(names, { ...argparseOptions }); - if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { - argumentParser.addArgument(parameter.undocumentedSynonyms, { - ...argparseOptions, - help: argparse.Const.SUPPRESS - }); + if (parameter.shortName) { + argumentParser.add_argument(parameter.shortName, parameter.longName, { ...argparseOptions }); + } else { + argumentParser.add_argument(parameter.longName, { ...argparseOptions }); + } + + if (parameter.undocumentedSynonyms) { + for (const undocumentedSynonym of parameter.undocumentedSynonyms) { + argumentParser.add_argument(undocumentedSynonym, { + ...argparseOptions, + help: argparse.SUPPRESS + }); + } } this._parameters.push(parameter); diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index f14888886ec..9ccf390a5ac 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -64,7 +64,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { this._actionsByName = new Map(); this._argumentParser = new CustomArgumentParser({ - addHelp: true, + add_help: true, prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( @@ -87,7 +87,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { */ public addAction(action: CommandLineAction): void { if (!this._actionsSubParser) { - this._actionsSubParser = this._argumentParser.addSubparsers({ + this._actionsSubParser = this._argumentParser.add_subparsers({ metavar: '', dest: 'action' }); @@ -195,11 +195,14 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { args = process.argv.slice(2); } if (args.length === 0) { - this._argumentParser.printHelp(); + this._argumentParser.print_help(); return; } - const data: ICommandLineParserData = this._argumentParser.parseArgs(args); + const data: ICommandLineParserData = { + parserOptions: this._options, + ...this._argumentParser.parse_args(args) + } this._processParsedData(data); diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap index ed13c337e1d..1fbf55655f3 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap @@ -542,63 +542,36 @@ Array [ `; exports[`CommandLineParameter prints the action help 1`] = ` -"usage: example do:the-job [-h] [-c {one,two,three,default}] - [--choice-with-default {one,two,three,default}] - [-C {red,green,blue}] [-f] [-i NUMBER] - [--integer-with-default NUMBER] --integer-required - NUMBER [-I LIST_ITEM] [-s TEXT] - [--string-with-default TEXT] - [--string-with-undocumented-synonym TEXT] - [-l LIST_ITEM] - +"usage: example do:the-job [-h] [-c {one,two,three,default}] [--choice-with-default {one,two,three,default}] [-C {red,green,blue}] [-f] [-i NUMBER] [--integer-with-default NUMBER] --integer-required NUMBER [-I LIST_ITEM] [-s TEXT] + [--string-with-default TEXT] [--string-with-undocumented-synonym TEXT] [-l LIST_ITEM] a longer description -Optional arguments: - -h, --help Show this help message and exit. +optional arguments: + -h, --help show this help message and exit -c {one,two,three,default}, --choice {one,two,three,default} - A choice. This parameter may alternatively be - specified via the ENV_CHOICE environment variable. + A choice. This parameter may alternatively be specified via the ENV_CHOICE environment variable. --choice-with-default {one,two,three,default} - A choice with a default. This description ends with a - \\"quoted word\\". This parameter may alternatively be - specified via the ENV_CHOICE2 environment variable. - The default value is \\"default\\". + A choice with a default. This description ends with a \\"quoted word\\". This parameter may alternatively be specified via the ENV_CHOICE2 environment variable. The default value is \\"default\\". -C {red,green,blue}, --choice-list {red,green,blue} - This parameter may be specified multiple times to - make a list of choices. This parameter may - alternatively be specified via the ENV_CHOICE_LIST - environment variable. - -f, --flag A flag. This parameter may alternatively be specified - via the ENV_FLAG environment variable. + This parameter may be specified multiple times to make a list of choices. This parameter may alternatively be specified via the ENV_CHOICE_LIST environment variable. + -f, --flag A flag. This parameter may alternatively be specified via the ENV_FLAG environment variable. -i NUMBER, --integer NUMBER - An integer. This parameter may alternatively be - specified via the ENV_INTEGER environment variable. + An integer. This parameter may alternatively be specified via the ENV_INTEGER environment variable. --integer-with-default NUMBER - An integer with a default. This parameter may - alternatively be specified via the ENV_INTEGER2 - environment variable. The default value is 123. + An integer with a default. This parameter may alternatively be specified via the ENV_INTEGER2 environment variable. The default value is 123. --integer-required NUMBER An integer -I LIST_ITEM, --integer-list LIST_ITEM - This parameter may be specified multiple times to - make a list of integers. This parameter may - alternatively be specified via the ENV_INTEGER_LIST - environment variable. + This parameter may be specified multiple times to make a list of integers. This parameter may alternatively be specified via the ENV_INTEGER_LIST environment variable. -s TEXT, --string TEXT - A string. This parameter may alternatively be - specified via the ENV_STRING environment variable. + A string. This parameter may alternatively be specified via the ENV_STRING environment variable. --string-with-default TEXT - A string with a default. This parameter may - alternatively be specified via the ENV_STRING2 - environment variable. The default value is \\"123\\". + A string with a default. This parameter may alternatively be specified via the ENV_STRING2 environment variable. The default value is \\"123\\". --string-with-undocumented-synonym TEXT A string with an undocumented synonym -l LIST_ITEM, --string-list LIST_ITEM - This parameter may be specified multiple times to - make a list of strings. This parameter may - alternatively be specified via the ENV_STRING_LIST - environment variable. + This parameter may be specified multiple times to make a list of strings. This parameter may alternatively be specified via the ENV_STRING_LIST environment variable. " `; @@ -607,12 +580,12 @@ exports[`CommandLineParameter prints the global help 1`] = ` An example project -Positional arguments: +positional arguments: do:the-job does the job -Optional arguments: - -h, --help Show this help message and exit. +optional arguments: + -h, --help show this help message and exit -g, --global-flag A flag that affects all actions For detailed help about a specific command, use: example -h 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..724c4350959 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,16 +12,26 @@ Array [ ] `; +exports[`CommandLineRemainder parses an action input with remainder 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] ... +"usage: example run [-h] [--title TEXT] a longer description -Positional arguments: - \\"...\\" The action remainder - -Optional arguments: - -h, --help Show this help message and exit. +optional arguments: + -h, --help show this help message and exit --title TEXT A string " `; @@ -31,12 +41,12 @@ exports[`CommandLineRemainder prints the global help 1`] = ` An example project -Positional arguments: +positional arguments: run does the job -Optional arguments: - -h, --help Show this help message and exit. +optional arguments: + -h, --help show this help message and exit --verbose A flag that affects all actions For detailed help about a specific command, use: example -h From eb33b9f7bb2a57e21a8659809d46e084c761906e Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Wed, 20 Apr 2022 15:33:15 -0700 Subject: [PATCH 02/16] Add new remainder test --- .../src/test/CommandLineRemainder.test.ts | 21 ++++++++ .../CommandLineParameter.test.ts.snap | 50 ++++++++++++++----- .../CommandLineRemainder.test.ts.snap | 9 ++-- 3 files changed, 65 insertions(+), 15 deletions(-) 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/__snapshots__/CommandLineParameter.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap index 1fbf55655f3..10f395c552c 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap @@ -542,36 +542,62 @@ Array [ `; exports[`CommandLineParameter prints the action help 1`] = ` -"usage: example do:the-job [-h] [-c {one,two,three,default}] [--choice-with-default {one,two,three,default}] [-C {red,green,blue}] [-f] [-i NUMBER] [--integer-with-default NUMBER] --integer-required NUMBER [-I LIST_ITEM] [-s TEXT] - [--string-with-default TEXT] [--string-with-undocumented-synonym TEXT] [-l LIST_ITEM] +"usage: example do:the-job [-h] [-c {one,two,three,default}] + [--choice-with-default {one,two,three,default}] + [-C {red,green,blue}] [-f] [-i NUMBER] + [--integer-with-default NUMBER] --integer-required + NUMBER [-I LIST_ITEM] [-s TEXT] + [--string-with-default TEXT] + [--string-with-undocumented-synonym TEXT] + [-l LIST_ITEM] a longer description optional arguments: -h, --help show this help message and exit -c {one,two,three,default}, --choice {one,two,three,default} - A choice. This parameter may alternatively be specified via the ENV_CHOICE environment variable. + A choice. This parameter may alternatively be + specified via the ENV_CHOICE environment variable. --choice-with-default {one,two,three,default} - A choice with a default. This description ends with a \\"quoted word\\". This parameter may alternatively be specified via the ENV_CHOICE2 environment variable. The default value is \\"default\\". + A choice with a default. This description ends with a + \\"quoted word\\". This parameter may alternatively be + specified via the ENV_CHOICE2 environment variable. + The default value is \\"default\\". -C {red,green,blue}, --choice-list {red,green,blue} - This parameter may be specified multiple times to make a list of choices. This parameter may alternatively be specified via the ENV_CHOICE_LIST environment variable. - -f, --flag A flag. This parameter may alternatively be specified via the ENV_FLAG environment variable. + This parameter may be specified multiple times to make + a list of choices. This parameter may alternatively be + specified via the ENV_CHOICE_LIST environment + variable. + -f, --flag A flag. This parameter may alternatively be specified + via the ENV_FLAG environment variable. -i NUMBER, --integer NUMBER - An integer. This parameter may alternatively be specified via the ENV_INTEGER environment variable. + An integer. This parameter may alternatively be + specified via the ENV_INTEGER environment variable. --integer-with-default NUMBER - An integer with a default. This parameter may alternatively be specified via the ENV_INTEGER2 environment variable. The default value is 123. + An integer with a default. This parameter may + alternatively be specified via the ENV_INTEGER2 + environment variable. The default value is 123. --integer-required NUMBER An integer -I LIST_ITEM, --integer-list LIST_ITEM - This parameter may be specified multiple times to make a list of integers. This parameter may alternatively be specified via the ENV_INTEGER_LIST environment variable. + This parameter may be specified multiple times to make + a list of integers. This parameter may alternatively + be specified via the ENV_INTEGER_LIST environment + variable. -s TEXT, --string TEXT - A string. This parameter may alternatively be specified via the ENV_STRING environment variable. + A string. This parameter may alternatively be + specified via the ENV_STRING environment variable. --string-with-default TEXT - A string with a default. This parameter may alternatively be specified via the ENV_STRING2 environment variable. The default value is \\"123\\". + A string with a default. This parameter may + alternatively be specified via the ENV_STRING2 + environment variable. The default value is \\"123\\". --string-with-undocumented-synonym TEXT A string with an undocumented synonym -l LIST_ITEM, --string-list LIST_ITEM - This parameter may be specified multiple times to make a list of strings. This parameter may alternatively be specified via the ENV_STRING_LIST environment variable. + This parameter may be specified multiple times to make + a list of strings. This parameter may alternatively be + specified via the ENV_STRING_LIST environment + variable. " `; 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 724c4350959..c7457eaab49 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,7 +12,7 @@ Array [ ] `; -exports[`CommandLineRemainder parses an action input with remainder options 1`] = ` +exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` Array [ "### --title output: ###", "--title", @@ -20,16 +20,19 @@ Array [ "### remainder output: ###", "--", "--the", - "--remaining", + "remaining", "--args", ] `; exports[`CommandLineRemainder prints the action help 1`] = ` -"usage: example run [-h] [--title TEXT] +"usage: example run [-h] [--title TEXT] ... a longer description +positional arguments: + \\"...\\" The action remainder + optional arguments: -h, --help show this help message and exit --title TEXT A string From e54d6c9b882f64efae4f273b6552ee9620d6f8ba Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Thu, 21 Apr 2022 12:49:36 -0700 Subject: [PATCH 03/16] Add ScopedCommandLineParser --- 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 | 81 ++++--- .../src/providers/CommandLineParser.ts | 16 +- .../src/providers/ScopedCommandLineAction.ts | 207 ++++++++++++++++++ .../src/test/CommandLineParameter.test.ts | 21 +- .../src/test/ScopedCommandLineAction.test.ts | 126 +++++++++++ .../CommandLineParameter.test.ts.snap | 50 +---- .../ScopedCommandLineAction.test.ts.snap | 47 ++++ 12 files changed, 509 insertions(+), 87 deletions(-) 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/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 c3e79814cbe..74d95f60460 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 73bd82030a2..23d76760202 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().format_help(); } + /** + * Generates the command-line usage text. + */ + public renderUsageText(): string { + return this._getArgumentParser().format_usage(); + } + /** * 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,16 +447,28 @@ export abstract class CommandLineParameterProvider { break; } - const argumentParser: argparse.ArgumentParser = this._getArgumentParser(); + let argumentGroup: argparse.ArgumentGroup | undefined; + if (parameter.groupName) { + argumentGroup = this._parameterGroupsByName.get(parameter.groupName); + if (!argumentGroup) { + argumentGroup = this._getArgumentParser().add_argument_group({ + title: `optional ${parameter.groupName} arguments` + }); + this._parameterGroupsByName.set(parameter.groupName, argumentGroup); + } + } else { + argumentGroup = this._getArgumentParser(); + } + if (parameter.shortName) { - argumentParser.add_argument(parameter.shortName, parameter.longName, { ...argparseOptions }); + argumentGroup.add_argument(parameter.shortName, parameter.longName, { ...argparseOptions }); } else { - argumentParser.add_argument(parameter.longName, { ...argparseOptions }); + argumentGroup.add_argument(parameter.longName, { ...argparseOptions }); } if (parameter.undocumentedSynonyms) { for (const undocumentedSynonym of parameter.undocumentedSynonyms) { - argumentParser.add_argument(undocumentedSynonym, { + argumentGroup.add_argument(undocumentedSynonym, { ...argparseOptions, help: argparse.SUPPRESS }); @@ -474,4 +478,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 9ccf390a5ac..786948c1a8a 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` ) }); @@ -200,16 +207,15 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { } const data: ICommandLineParserData = { - parserOptions: this._options, ...this._argumentParser.parse_args(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..eb121af0550 --- /dev/null +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -0,0 +1,207 @@ +// 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 actionName: string; + 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 scopedCommandLineParserOptions: ICommandLineParserOptions = { + toolFilename: + `${options.toolFilename} ${options.actionName}` + + `${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, + toolDescription: options.toolDescription, + toolEpilog: + 'For more information on available unscoped parameters, use ' + + `"${options.toolFilename} ${options.actionName} --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 CommandLineAction 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). + * + * @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, + ...this._options, + unscopedActionParameters: this.parameters, + onDefineScopedParameters: this.onDefineScopedParameters.bind(this), + onExecute: this.onExecute.bind(this) + }); + } + + /** + * {@inheritdoc CommandLineAction._execute} + * @internal + */ + public _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] !== '--') { + 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. + return this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + } + + /** + * {@inheritdoc CommandLineParameterProvider.onDefineParameters} + */ + protected onDefineParameters(): void { + this.onDefineUnscopedParameters(); + + if (!this._scopingParameters.length) { + throw new Error( + 'No scoping parameters defined. At least one parameter with the groupName set to ' + + 'ScopedCommandLineAction.ScopingParameterGroupName must be defined.' + ); + } + 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. "-- --scoped-parameter ' + + 'foo --scoped-flag". For more information on available scoped parameters, use "-- --help" ' + + 'on the scoped command.' + }); + } + + /** + * 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 scoping command-line parameters + * and its unscoped command-line parameters, e.g. by calling defineScopingFlagParameter() + * and defineFlagParameter(), respectively. At least one scoping parameter must be defined. + */ + 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. + */ + 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/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts new file mode 100644 index 00000000000..e09d7926ec4 --- /dev/null +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -0,0 +1,126 @@ +// 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 arg', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scoped-foo', 'bar']; + + return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + }); + + 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', async () => { + const commandLineParser: TestCommandLine = new TestCommandLine(); + const args: string[] = ['scoped-action', '--scope', 'foo', '--scoped-foo', 'bar']; + + 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__/CommandLineParameter.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap index 10f395c552c..1fbf55655f3 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap @@ -542,62 +542,36 @@ Array [ `; exports[`CommandLineParameter prints the action help 1`] = ` -"usage: example do:the-job [-h] [-c {one,two,three,default}] - [--choice-with-default {one,two,three,default}] - [-C {red,green,blue}] [-f] [-i NUMBER] - [--integer-with-default NUMBER] --integer-required - NUMBER [-I LIST_ITEM] [-s TEXT] - [--string-with-default TEXT] - [--string-with-undocumented-synonym TEXT] - [-l LIST_ITEM] +"usage: example do:the-job [-h] [-c {one,two,three,default}] [--choice-with-default {one,two,three,default}] [-C {red,green,blue}] [-f] [-i NUMBER] [--integer-with-default NUMBER] --integer-required NUMBER [-I LIST_ITEM] [-s TEXT] + [--string-with-default TEXT] [--string-with-undocumented-synonym TEXT] [-l LIST_ITEM] a longer description optional arguments: -h, --help show this help message and exit -c {one,two,three,default}, --choice {one,two,three,default} - A choice. This parameter may alternatively be - specified via the ENV_CHOICE environment variable. + A choice. This parameter may alternatively be specified via the ENV_CHOICE environment variable. --choice-with-default {one,two,three,default} - A choice with a default. This description ends with a - \\"quoted word\\". This parameter may alternatively be - specified via the ENV_CHOICE2 environment variable. - The default value is \\"default\\". + A choice with a default. This description ends with a \\"quoted word\\". This parameter may alternatively be specified via the ENV_CHOICE2 environment variable. The default value is \\"default\\". -C {red,green,blue}, --choice-list {red,green,blue} - This parameter may be specified multiple times to make - a list of choices. This parameter may alternatively be - specified via the ENV_CHOICE_LIST environment - variable. - -f, --flag A flag. This parameter may alternatively be specified - via the ENV_FLAG environment variable. + This parameter may be specified multiple times to make a list of choices. This parameter may alternatively be specified via the ENV_CHOICE_LIST environment variable. + -f, --flag A flag. This parameter may alternatively be specified via the ENV_FLAG environment variable. -i NUMBER, --integer NUMBER - An integer. This parameter may alternatively be - specified via the ENV_INTEGER environment variable. + An integer. This parameter may alternatively be specified via the ENV_INTEGER environment variable. --integer-with-default NUMBER - An integer with a default. This parameter may - alternatively be specified via the ENV_INTEGER2 - environment variable. The default value is 123. + An integer with a default. This parameter may alternatively be specified via the ENV_INTEGER2 environment variable. The default value is 123. --integer-required NUMBER An integer -I LIST_ITEM, --integer-list LIST_ITEM - This parameter may be specified multiple times to make - a list of integers. This parameter may alternatively - be specified via the ENV_INTEGER_LIST environment - variable. + This parameter may be specified multiple times to make a list of integers. This parameter may alternatively be specified via the ENV_INTEGER_LIST environment variable. -s TEXT, --string TEXT - A string. This parameter may alternatively be - specified via the ENV_STRING environment variable. + A string. This parameter may alternatively be specified via the ENV_STRING environment variable. --string-with-default TEXT - A string with a default. This parameter may - alternatively be specified via the ENV_STRING2 - environment variable. The default value is \\"123\\". + A string with a default. This parameter may alternatively be specified via the ENV_STRING2 environment variable. The default value is \\"123\\". --string-with-undocumented-synonym TEXT A string with an undocumented synonym -l LIST_ITEM, --string-list LIST_ITEM - This parameter may be specified multiple times to make - a list of strings. This parameter may alternatively be - specified via the ENV_STRING_LIST environment - variable. + This parameter may be specified multiple times to make a list of strings. This parameter may alternatively be specified via the ENV_STRING_LIST environment variable. " `; 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..6e540ef18a4 --- /dev/null +++ b/libraries/ts-command-line/src/test/__snapshots__/ScopedCommandLineAction.test.ts.snap @@ -0,0 +1,47 @@ +// 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. \\"-- --scoped-parameter foo --scoped-flag\\". For more information on available scoped parameters, use \\"-- --help\\" on the scoped command. + +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] + +An example project + +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 1`] = ` +"example: error: unrecognized arguments: --scoped-foo +" +`; + +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 arg 1`] = ` +"example: error: unrecognized arguments: --scoped-foo +" +`; + +exports[`CommandLineParser throws on unknown scoped arg 1`] = ` +"example scoped-action --scope foo --: error: unrecognized arguments: --scoped-bar baz +" +`; From 09a59cd7c5eed08c781d9ec5884cbdd835ae4c81 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Thu, 21 Apr 2022 13:05:40 -0700 Subject: [PATCH 04/16] Revert "Update argparse version" This reverts commit 6f917d351eede7297e65132dbb3f33a3622be6c3. --- libraries/ts-command-line/package.json | 4 +- .../src/providers/CommandLineAction.ts | 2 +- .../providers/CommandLineParameterProvider.ts | 48 +++++++-------- .../src/providers/CommandLineParser.ts | 10 ++- .../src/providers/ScopedCommandLineAction.ts | 4 +- .../src/test/ScopedCommandLineAction.test.ts | 16 +---- .../CommandLineParameter.test.ts.snap | 61 +++++++++++++------ .../CommandLineRemainder.test.ts.snap | 12 ++-- .../ScopedCommandLineAction.test.ts.snap | 36 +++++------ 9 files changed, 96 insertions(+), 97 deletions(-) diff --git a/libraries/ts-command-line/package.json b/libraries/ts-command-line/package.json index e389f405916..cab7c32bc5f 100644 --- a/libraries/ts-command-line/package.json +++ b/libraries/ts-command-line/package.json @@ -16,8 +16,8 @@ }, "license": "MIT", "dependencies": { - "@types/argparse": "2.0.10", - "argparse": "~2.0.1", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", "colors": "~1.2.1", "string-argv": "~0.3.1" }, diff --git a/libraries/ts-command-line/src/providers/CommandLineAction.ts b/libraries/ts-command-line/src/providers/CommandLineAction.ts index 74d95f60460..e9f5fb874ea 100644 --- a/libraries/ts-command-line/src/providers/CommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/CommandLineAction.ts @@ -80,7 +80,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider { * @internal */ public _buildParser(actionsSubParser: argparse.SubParser): void { - this._argumentParser = actionsSubParser.add_parser(this.actionName, { + this._argumentParser = actionsSubParser.addParser(this.actionName, { help: this.summary, description: this.documentation }); diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 23d76760202..c25658e2f07 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -277,11 +277,11 @@ export abstract class CommandLineParameterProvider { const argparseOptions: argparse.ArgumentOptions = { help: this._remainder.description, - nargs: argparse.REMAINDER, + nargs: argparse.Const.REMAINDER, metavar: '"..."' }; - this._getArgumentParser().add_argument(argparse.REMAINDER, argparseOptions); + this._getArgumentParser().addArgument(argparse.Const.REMAINDER, argparseOptions); return this._remainder; } @@ -299,14 +299,14 @@ export abstract class CommandLineParameterProvider { * Generates the command-line help text. */ public renderHelpText(): string { - return this._getArgumentParser().format_help(); + return this._getArgumentParser().formatHelp(); } /** * Generates the command-line usage text. */ public renderUsageText(): string { - return this._getArgumentParser().format_usage(); + return this._getArgumentParser().formatUsage(); } /** @@ -373,7 +373,7 @@ export abstract class CommandLineParameterProvider { } if (this.remainder) { - this.remainder._setValue(data[argparse.REMAINDER]); + this.remainder._setValue(data[argparse.Const.REMAINDER]); } this._parametersProcessed = true; @@ -388,6 +388,12 @@ export abstract class CommandLineParameterProvider { ); } + const names: string[] = []; + if (parameter.shortName) { + names.push(parameter.shortName); + } + names.push(parameter.longName); + parameter._parserKey = this._generateKey(); let finalDescription: string = parameter.description; @@ -408,16 +414,10 @@ export abstract class CommandLineParameterProvider { const argparseOptions: argparse.ArgumentOptions = { help: finalDescription, dest: parameter._parserKey, + metavar: (parameter as CommandLineParameterWithArgument).argumentName || undefined, required: parameter.required }; - // Only add the metavar if it's specified. Setting to undefined will cause argparse to throw when - // metavar isn't - const metavarValue: string | undefined = (parameter as CommandLineParameterWithArgument).argumentName; - if (metavarValue) { - argparseOptions.metavar = metavarValue; - } - switch (parameter.kind) { case CommandLineParameterKind.Choice: { const choiceParameter: CommandLineChoiceParameter = parameter as CommandLineChoiceParameter; @@ -431,7 +431,7 @@ export abstract class CommandLineParameterProvider { break; } case CommandLineParameterKind.Flag: - argparseOptions.action = 'store_true'; + argparseOptions.action = 'storeTrue'; break; case CommandLineParameterKind.Integer: argparseOptions.type = 'int'; @@ -451,8 +451,8 @@ export abstract class CommandLineParameterProvider { if (parameter.groupName) { argumentGroup = this._parameterGroupsByName.get(parameter.groupName); if (!argumentGroup) { - argumentGroup = this._getArgumentParser().add_argument_group({ - title: `optional ${parameter.groupName} arguments` + argumentGroup = this._getArgumentParser().addArgumentGroup({ + title: `Optional ${parameter.groupName} arguments` }); this._parameterGroupsByName.set(parameter.groupName, argumentGroup); } @@ -460,19 +460,13 @@ export abstract class CommandLineParameterProvider { argumentGroup = this._getArgumentParser(); } - if (parameter.shortName) { - argumentGroup.add_argument(parameter.shortName, parameter.longName, { ...argparseOptions }); - } else { - argumentGroup.add_argument(parameter.longName, { ...argparseOptions }); - } + argumentGroup.addArgument(names, { ...argparseOptions }); - if (parameter.undocumentedSynonyms) { - for (const undocumentedSynonym of parameter.undocumentedSynonyms) { - argumentGroup.add_argument(undocumentedSynonym, { - ...argparseOptions, - help: argparse.SUPPRESS - }); - } + if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) { + argumentGroup.addArgument(parameter.undocumentedSynonyms, { + ...argparseOptions, + help: argparse.Const.SUPPRESS + }); } this._parameters.push(parameter); diff --git a/libraries/ts-command-line/src/providers/CommandLineParser.ts b/libraries/ts-command-line/src/providers/CommandLineParser.ts index 786948c1a8a..204f40e5b13 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParser.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParser.ts @@ -70,7 +70,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { this._actionsByName = new Map(); this._argumentParser = new CustomArgumentParser({ - add_help: true, + addHelp: true, prog: this._options.toolFilename, description: this._options.toolDescription, epilog: colors.bold( @@ -94,7 +94,7 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { */ public addAction(action: CommandLineAction): void { if (!this._actionsSubParser) { - this._actionsSubParser = this._argumentParser.add_subparsers({ + this._actionsSubParser = this._argumentParser.addSubparsers({ metavar: '', dest: 'action' }); @@ -202,13 +202,11 @@ export abstract class CommandLineParser extends CommandLineParameterProvider { args = process.argv.slice(2); } if (args.length === 0) { - this._argumentParser.print_help(); + this._argumentParser.printHelp(); return; } - const data: ICommandLineParserData = { - ...this._argumentParser.parse_args(args) - }; + const data: ICommandLineParserData = this._argumentParser.parseArgs(args); this._processParsedData(this._options, data); diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index eb121af0550..cf171e21071 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -126,8 +126,8 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { // 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]}` + `${this._unscopedParserOptions.toolFilename} ${this.actionName}: error: Unrecognized ` + + `arguments: ${this.remainder.values[0]}.` ); } scopedArgs.push(...this.remainder.values.slice(1)); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts index e09d7926ec4..2caaaf3ffc5 100644 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -41,7 +41,7 @@ class TestScopedAction extends ScopedCommandLineAction { this._scopedArg = scopedParameterProvider.defineStringParameter({ parameterLongName: `--scoped-${this._scopeArg.value}`, argumentName: 'SCOPED', - description: 'The scoped argument' + description: 'The scoped argument.' }); } } @@ -62,13 +62,6 @@ class TestCommandLine extends CommandLineParser { } describe(CommandLineParser.name, () => { - it('throws on unknown arg', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const args: string[] = ['scoped-action', '--scoped-foo', 'bar']; - - return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); - }); - it('throws on unknown scoped arg', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; @@ -76,13 +69,6 @@ describe(CommandLineParser.name, () => { return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); - it('throws on missing positional arg divider', async () => { - const commandLineParser: TestCommandLine = new TestCommandLine(); - const args: string[] = ['scoped-action', '--scope', 'foo', '--scoped-foo', 'bar']; - - 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']; diff --git a/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap index 1fbf55655f3..ed13c337e1d 100644 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap +++ b/libraries/ts-command-line/src/test/__snapshots__/CommandLineParameter.test.ts.snap @@ -542,36 +542,63 @@ Array [ `; exports[`CommandLineParameter prints the action help 1`] = ` -"usage: example do:the-job [-h] [-c {one,two,three,default}] [--choice-with-default {one,two,three,default}] [-C {red,green,blue}] [-f] [-i NUMBER] [--integer-with-default NUMBER] --integer-required NUMBER [-I LIST_ITEM] [-s TEXT] - [--string-with-default TEXT] [--string-with-undocumented-synonym TEXT] [-l LIST_ITEM] +"usage: example do:the-job [-h] [-c {one,two,three,default}] + [--choice-with-default {one,two,three,default}] + [-C {red,green,blue}] [-f] [-i NUMBER] + [--integer-with-default NUMBER] --integer-required + NUMBER [-I LIST_ITEM] [-s TEXT] + [--string-with-default TEXT] + [--string-with-undocumented-synonym TEXT] + [-l LIST_ITEM] + a longer description -optional arguments: - -h, --help show this help message and exit +Optional arguments: + -h, --help Show this help message and exit. -c {one,two,three,default}, --choice {one,two,three,default} - A choice. This parameter may alternatively be specified via the ENV_CHOICE environment variable. + A choice. This parameter may alternatively be + specified via the ENV_CHOICE environment variable. --choice-with-default {one,two,three,default} - A choice with a default. This description ends with a \\"quoted word\\". This parameter may alternatively be specified via the ENV_CHOICE2 environment variable. The default value is \\"default\\". + A choice with a default. This description ends with a + \\"quoted word\\". This parameter may alternatively be + specified via the ENV_CHOICE2 environment variable. + The default value is \\"default\\". -C {red,green,blue}, --choice-list {red,green,blue} - This parameter may be specified multiple times to make a list of choices. This parameter may alternatively be specified via the ENV_CHOICE_LIST environment variable. - -f, --flag A flag. This parameter may alternatively be specified via the ENV_FLAG environment variable. + This parameter may be specified multiple times to + make a list of choices. This parameter may + alternatively be specified via the ENV_CHOICE_LIST + environment variable. + -f, --flag A flag. This parameter may alternatively be specified + via the ENV_FLAG environment variable. -i NUMBER, --integer NUMBER - An integer. This parameter may alternatively be specified via the ENV_INTEGER environment variable. + An integer. This parameter may alternatively be + specified via the ENV_INTEGER environment variable. --integer-with-default NUMBER - An integer with a default. This parameter may alternatively be specified via the ENV_INTEGER2 environment variable. The default value is 123. + An integer with a default. This parameter may + alternatively be specified via the ENV_INTEGER2 + environment variable. The default value is 123. --integer-required NUMBER An integer -I LIST_ITEM, --integer-list LIST_ITEM - This parameter may be specified multiple times to make a list of integers. This parameter may alternatively be specified via the ENV_INTEGER_LIST environment variable. + This parameter may be specified multiple times to + make a list of integers. This parameter may + alternatively be specified via the ENV_INTEGER_LIST + environment variable. -s TEXT, --string TEXT - A string. This parameter may alternatively be specified via the ENV_STRING environment variable. + A string. This parameter may alternatively be + specified via the ENV_STRING environment variable. --string-with-default TEXT - A string with a default. This parameter may alternatively be specified via the ENV_STRING2 environment variable. The default value is \\"123\\". + A string with a default. This parameter may + alternatively be specified via the ENV_STRING2 + environment variable. The default value is \\"123\\". --string-with-undocumented-synonym TEXT A string with an undocumented synonym -l LIST_ITEM, --string-list LIST_ITEM - This parameter may be specified multiple times to make a list of strings. This parameter may alternatively be specified via the ENV_STRING_LIST environment variable. + This parameter may be specified multiple times to + make a list of strings. This parameter may + alternatively be specified via the ENV_STRING_LIST + environment variable. " `; @@ -580,12 +607,12 @@ exports[`CommandLineParameter prints the global help 1`] = ` An example project -positional arguments: +Positional arguments: do:the-job does the job -optional arguments: - -h, --help show this help message and exit +Optional arguments: + -h, --help Show this help message and exit. -g, --global-flag A flag that affects all actions For detailed help about a specific command, use: example -h 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 c7457eaab49..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 @@ -30,11 +30,11 @@ exports[`CommandLineRemainder prints the action help 1`] = ` a longer description -positional arguments: +Positional arguments: \\"...\\" The action remainder -optional arguments: - -h, --help show this help message and exit +Optional arguments: + -h, --help Show this help message and exit. --title TEXT A string " `; @@ -44,12 +44,12 @@ exports[`CommandLineRemainder prints the global help 1`] = ` An example project -positional arguments: +Positional arguments: run does the job -optional arguments: - -h, --help show this help message and exit +Optional arguments: + -h, --help Show this help message and exit. --verbose A flag that affects all actions For detailed help about a specific command, use: example -h 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 6e540ef18a4..9cf40694f62 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 @@ -5,13 +5,16 @@ exports[`CommandLineParser prints the action help 1`] = ` a longer description -positional arguments: - \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- --scoped-parameter foo --scoped-flag\\". For more information on available scoped parameters, use \\"-- --help\\" on the scoped command. +Positional arguments: + \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- + --scoped-parameter foo --scoped-flag\\". For more information + on available scoped parameters, use \\"-- --help\\" on the + scoped command. -optional arguments: - -h, --help show this help message and exit +Optional arguments: + -h, --help Show this help message and exit. -optional scoping arguments: +Optional scoping arguments: --scope SCOPE The scope " `; @@ -21,27 +24,18 @@ exports[`CommandLineParser prints the scoped action help 1`] = ` An example project -optional arguments: - -h, --help show this help message and exit - --scoped-foo SCOPED The scoped argument +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\\" +For more information on available unscoped parameters, use \\"example +scoped-action --help\\" " `; -exports[`CommandLineParser throws on missing positional arg divider 1`] = ` -"example: error: unrecognized arguments: --scoped-foo -" -`; - -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 arg 1`] = ` -"example: error: unrecognized arguments: --scoped-foo -" -`; +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 +"example scoped-action --scope foo --: error: Unrecognized arguments: --scoped-bar baz. " `; From 3b80ce50ea7939cb1f2164855411675d1637d293 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Thu, 21 Apr 2022 14:29:27 -0700 Subject: [PATCH 05/16] Cleanup --- .../src/providers/ScopedCommandLineAction.ts | 36 +++++++++++-------- .../ScopedCommandLineAction.test.ts.snap | 2 +- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index cf171e21071..528238f3fe6 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -8,7 +8,7 @@ import type { CommandLineParameter } from '../parameters/BaseClasses'; import type { CommandLineParameterProvider, ICommandLineParserData } from './CommandLineParameterProvider'; interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { - readonly actionName: string; + readonly actionOptions: ICommandLineActionOptions; readonly unscopedActionParameters: ReadonlyArray; readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; readonly onExecute: () => Promise; @@ -24,14 +24,12 @@ class InternalScopedCommandLineParser extends CommandLineParser { for (const parameter of options.unscopedActionParameters) { parameter.appendToArgList(scopingArgs); } + const unscopedToolName: string = `${options.toolFilename} ${options.actionOptions.actionName}`; const scopedCommandLineParserOptions: ICommandLineParserOptions = { - toolFilename: - `${options.toolFilename} ${options.actionName}` + - `${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, - toolDescription: options.toolDescription, + toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, + toolDescription: options.actionOptions.documentation, toolEpilog: - 'For more information on available unscoped parameters, use ' + - `"${options.toolFilename} ${options.actionName} --help"`, + 'For more information on available unscoped parameters, use ' + `"${unscopedToolName} --help"`, enableTabCompletionAction: false }; @@ -96,7 +94,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { // have parsed the data, since the parameter values are used during construction. this._scopedCommandLineParser = new InternalScopedCommandLineParser({ ...parserOptions, - ...this._options, + actionOptions: this._options, unscopedActionParameters: this.parameters, onDefineScopedParameters: this.onDefineScopedParameters.bind(this), onExecute: this.onExecute.bind(this) @@ -107,7 +105,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { * {@inheritdoc CommandLineAction._execute} * @internal */ - public _execute(): Promise { + public async _execute(): Promise { // override if (!this._unscopedParserOptions || !this._scopedCommandLineParser) { throw new Error('The CommandLineAction parameters must be processed before execution.'); @@ -121,6 +119,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { 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 @@ -134,7 +133,8 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { } // Call the scoped parser using only the scoped args. - return this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + return; } /** @@ -145,8 +145,9 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { if (!this._scopingParameters.length) { throw new Error( - 'No scoping parameters defined. At least one parameter with the groupName set to ' + - 'ScopedCommandLineAction.ScopingParameterGroupName must be defined.' + '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) { @@ -187,9 +188,10 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { } /** - * The child class should implement this hook to define its scoping command-line parameters - * and its unscoped command-line parameters, e.g. by calling defineScopingFlagParameter() - * and defineFlagParameter(), respectively. At least one scoping parameter must be defined. + * 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; @@ -197,6 +199,10 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { * 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; 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 9cf40694f62..9ff004de60a 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 @@ -22,7 +22,7 @@ Optional scoping arguments: exports[`CommandLineParser prints the scoped action help 1`] = ` "usage: example scoped-action --scope foo -- [-h] [--scoped-foo SCOPED] -An example project +a longer description Optional arguments: -h, --help Show this help message and exit. From 34cef79a18fb9a174c8fbb71b46892ba1309ea1f Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Thu, 21 Apr 2022 14:55:59 -0700 Subject: [PATCH 06/16] More cleanup --- .../src/providers/ScopedCommandLineAction.ts | 8 +++----- .../__snapshots__/ScopedCommandLineAction.test.ts.snap | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index 528238f3fe6..e82dccfa260 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -28,8 +28,7 @@ class InternalScopedCommandLineParser extends CommandLineParser { const scopedCommandLineParserOptions: ICommandLineParserOptions = { toolFilename: `${unscopedToolName}${scopingArgs.length ? ' ' + scopingArgs.join(' ') : ''} --`, toolDescription: options.actionOptions.documentation, - toolEpilog: - 'For more information on available unscoped parameters, use ' + `"${unscopedToolName} --help"`, + toolEpilog: `For more information on available unscoped parameters, use "${unscopedToolName} --help"`, enableTabCompletionAction: false }; @@ -162,9 +161,8 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { // we will have already defined it. this.defineCommandLineRemainder({ description: - 'Scoped parameters. Must be prefixed with "--", ex. "-- --scoped-parameter ' + - 'foo --scoped-flag". For more information on available scoped parameters, use "-- --help" ' + - 'on the scoped command.' + 'Scoped parameters. Must be prefixed with "--", ex. "-- --scopedParameter ' + + 'foo --scopedFlag". For more information on available scoped parameters, use "-- --help".' }); } 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 9ff004de60a..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 @@ -7,9 +7,8 @@ a longer description Positional arguments: \\"...\\" Scoped parameters. Must be prefixed with \\"--\\", ex. \\"-- - --scoped-parameter foo --scoped-flag\\". For more information - on available scoped parameters, use \\"-- --help\\" on the - scoped command. + --scopedParameter foo --scopedFlag\\". For more information on + available scoped parameters, use \\"-- --help\\". Optional arguments: -h, --help Show this help message and exit. From a363cf4d03cebad7bd626eea27c5221b9de00a13 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Thu, 21 Apr 2022 15:00:27 -0700 Subject: [PATCH 07/16] Rush change --- ...user-danade-ScopedCommandLine_2022-04-21-22-00.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/ts-command-line/user-danade-ScopedCommandLine_2022-04-21-22-00.json 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 From bdfd029c40886a7539d6a5a9f8207a358b28af0c Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Thu, 21 Apr 2022 15:12:11 -0700 Subject: [PATCH 08/16] Updated docstring --- .../src/providers/ScopedCommandLineAction.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index e82dccfa260..6a713b84c89 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -49,7 +49,7 @@ class InternalScopedCommandLineParser extends CommandLineParser { /** * Represents a sub-command that is part of the CommandLineParser command-line. - * Applications should create subclasses of CommandLineAction corresponding to + * 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 @@ -58,6 +58,14 @@ class InternalScopedCommandLineParser extends CommandLineParser { * 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 { From d810a9076fd54ba069abe23f44a548321b59c852 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 09/16] 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 9e4e222483949a7e1ab11be048313419d737f75e Mon Sep 17 00:00:00 2001 From: Daniel <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 17:32:04 -0700 Subject: [PATCH 10/16] Apply suggestions from code review Co-authored-by: Ian Clanton-Thuon --- .../user-danade-ScopedCommandLine_2022-04-21-22-00.json | 2 +- .../ts-command-line/src/test/CommandLineParameter.test.ts | 6 +++--- .../src/test/ScopedCommandLineAction.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) 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 index d12bdde165b..7f0cc08de2a 100644 --- 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 @@ -2,7 +2,7 @@ "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.", + "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" } ], diff --git a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts index 82fe7ddc79d..af389928f9a 100644 --- a/libraries/ts-command-line/src/test/CommandLineParameter.test.ts +++ b/libraries/ts-command-line/src/test/CommandLineParameter.test.ts @@ -366,7 +366,7 @@ describe(CommandLineParameter.name, () => { const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[u'; - return expect( + await expect( commandLineParser.executeWithoutErrorHandling(args) ).rejects.toThrowErrorMatchingSnapshot(); }); @@ -376,7 +376,7 @@ describe(CommandLineParameter.name, () => { const args: string[] = ['hello-world']; process.env.ENV_COLOR = '[{}]'; - return expect( + await expect( commandLineParser.executeWithoutErrorHandling(args) ).rejects.toThrowErrorMatchingSnapshot(); }); @@ -386,7 +386,7 @@ describe(CommandLineParameter.name, () => { const args: string[] = ['hello-world']; process.env.ENV_COLOR = 'oblong'; - return expect( + await expect( commandLineParser.executeWithoutErrorHandling(args) ).rejects.toThrowErrorMatchingSnapshot(); }); diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts index 6998dcd565f..88110fffab7 100644 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -77,14 +77,14 @@ describe(CommandLineParser.name, () => { const commandLineParser: TestCommandLine = new TestCommandLine(); const args: string[] = ['scoped-action', '--scope', 'foo', '--', '--scoped-bar', 'baz']; - return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + 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']; - return expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); + await expect(commandLineParser.executeWithoutErrorHandling(args)).rejects.toThrowErrorMatchingSnapshot(); }); it('executes a scoped action', async () => { @@ -110,7 +110,7 @@ describe(CommandLineParser.name, () => { it('prints the scoped action help', async () => { const commandLineParser: TestCommandLine = new TestCommandLine(); - // Execute the parser in order to populate the scoped action + // 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 & { From 1b8282a23f63cb51f9b1d25fb4457f816dd2f634 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 6 May 2022 17:38:39 -0700 Subject: [PATCH 11/16] Use a symbol for the 'scoping' parameter group --- common/reviews/api/ts-command-line.api.md | 6 ++--- .../src/parameters/BaseClasses.ts | 7 +++--- .../src/parameters/CommandLineDefinition.ts | 4 +++- .../providers/CommandLineParameterProvider.ts | 24 +++++++++++++------ .../src/providers/ScopedCommandLineAction.ts | 6 +++-- .../src/test/ScopedCommandLineAction.test.ts | 2 +- 6 files changed, 32 insertions(+), 17 deletions(-) diff --git a/common/reviews/api/ts-command-line.api.md b/common/reviews/api/ts-command-line.api.md index 55425e6b97f..c9964e7a8ab 100644 --- a/common/reviews/api/ts-command-line.api.md +++ b/common/reviews/api/ts-command-line.api.md @@ -113,9 +113,9 @@ export abstract class CommandLineParameter { readonly environmentVariable: string | undefined; // @internal _getSupplementaryNotes(supplementaryNotes: string[]): void; - readonly groupName: string | undefined; abstract get kind(): CommandLineParameterKind; readonly longName: string; + readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; // @internal _parserKey: string | undefined; protected reportInvalidData(data: any): never; @@ -253,7 +253,7 @@ export class DynamicCommandLineParser extends CommandLineParser { export interface IBaseCommandLineDefinition { description: string; environmentVariable?: string; - parameterGroupName?: string; + parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP; parameterLongName: string; parameterShortName?: string; required?: boolean; @@ -345,7 +345,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { get parameters(): ReadonlyArray; // @internal _processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void; - static ScopingParameterGroupName: 'scoping'; + static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP; } ``` diff --git a/libraries/ts-command-line/src/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 2c7c4e1f4d7..95a1e2c3fb0 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 { SCOPING_PARAMETER_GROUP } from '../providers/ScopedCommandLineAction'; import { IBaseCommandLineDefinition, IBaseCommandLineDefinitionWithArgument } from './CommandLineDefinition'; /** @@ -53,8 +54,8 @@ export abstract class CommandLineParameter { /** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */ public readonly shortName: string | undefined; - /** {@inheritDoc IBaseCommandLineDefinition.parameterGroupName} */ - public readonly groupName: string | undefined; + /** {@inheritDoc IBaseCommandLineDefinition.parameterGroup} */ + public readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined; /** {@inheritDoc IBaseCommandLineDefinition.description} */ public readonly description: string; @@ -72,7 +73,7 @@ export abstract class CommandLineParameter { public constructor(definition: IBaseCommandLineDefinition) { this.longName = definition.parameterLongName; this.shortName = definition.parameterShortName; - this.groupName = definition.parameterGroupName; + 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 04e5e72894a..68af29cfb61 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 { SCOPING_PARAMETER_GROUP } from '../providers/ScopedCommandLineAction'; + /** * For use with CommandLineParser, this interface represents a generic command-line parameter * @@ -20,7 +22,7 @@ export interface IBaseCommandLineDefinition { /** * An optional parameter group name, shown when invoking the tool with "--help" */ - parameterGroupName?: string; + 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/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index c25658e2f07..6e5dabbabf5 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -27,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 './ScopedCommandLineAction'; /** * This is the argparse result data object @@ -48,7 +49,7 @@ export abstract class CommandLineParameterProvider { private _parameters: CommandLineParameter[]; private _parametersByLongName: Map; - private _parameterGroupsByName: Map; + private _parameterGroupsByName: Map; private _parametersProcessed: boolean; private _remainder: CommandLineRemainder | undefined; @@ -56,8 +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._parameterGroupsByName = new Map(); + this._parametersByLongName = new Map(); + this._parameterGroupsByName = new Map(); this._parametersProcessed = false; } @@ -448,13 +449,22 @@ export abstract class CommandLineParameterProvider { } let argumentGroup: argparse.ArgumentGroup | undefined; - if (parameter.groupName) { - argumentGroup = this._parameterGroupsByName.get(parameter.groupName); + 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 ${parameter.groupName} arguments` + title: `Optional ${parameterGroupName} arguments` }); - this._parameterGroupsByName.set(parameter.groupName, argumentGroup); + this._parameterGroupsByName.set(parameter.parameterGroup, argumentGroup); } } else { argumentGroup = this._getArgumentParser(); diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index 53b35819160..a1fd49dc5f3 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -48,6 +48,8 @@ class InternalScopedCommandLineParser extends CommandLineParser { } } +export const SCOPING_PARAMETER_GROUP: unique symbol = Symbol('scoping'); + /** * Represents a sub-command that is part of the CommandLineParser command-line. * Applications should create subclasses of ScopedCommandLineAction corresponding to @@ -79,7 +81,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { * 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 static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP = SCOPING_PARAMETER_GROUP; public constructor(options: ICommandLineActionOptions) { super(options); @@ -200,7 +202,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { /** @internal */ protected _defineParameter(parameter: CommandLineParameter): void { super._defineParameter(parameter); - if (parameter.groupName === ScopedCommandLineAction.ScopingParameterGroupName) { + if (parameter.parameterGroup === ScopedCommandLineAction.ScopingParameterGroup) { this._scopingParameters.push(parameter); } } diff --git a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts index 6998dcd565f..1aed3d8cfcd 100644 --- a/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts +++ b/libraries/ts-command-line/src/test/ScopedCommandLineAction.test.ts @@ -40,7 +40,7 @@ class TestScopedAction extends ScopedCommandLineAction { this._scopeArg = this.defineStringParameter({ parameterLongName: '--scope', - parameterGroupName: ScopedCommandLineAction.ScopingParameterGroupName, + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup, argumentName: 'SCOPE', description: 'The scope' }); From 9a54ec26c297d7c233f0ec4242b5c9ac415080ad Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 17:47:25 -0700 Subject: [PATCH 12/16] Call the onExecute directly --- .../src/providers/ScopedCommandLineAction.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index 53b35819160..fed3fa23377 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -11,9 +11,12 @@ interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOpti readonly actionOptions: ICommandLineActionOptions; readonly unscopedActionParameters: ReadonlyArray; readonly onDefineScopedParameters: (commandLineParameterProvider: CommandLineParameterProvider) => void; - readonly onExecute: () => Promise; } +/** + * A CommandLineParser used exclusively to parse the scoped command-line parameters + * for a ScopedCommandLineAction. + */ class InternalScopedCommandLineParser extends CommandLineParser { private _internalOptions: IInternalScopedCommandLineParserOptions; @@ -40,12 +43,6 @@ class InternalScopedCommandLineParser extends CommandLineParser { protected onDefineParameters(): void { // No-op. Parameters are manually defined in the constructor. } - - protected async onExecute(): Promise { - await super.onExecute(); - // Redirect action execution to the provided callback - await this._internalOptions.onExecute(); - } } /** @@ -115,8 +112,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { ...parserOptions, actionOptions: this._options, unscopedActionParameters: this.parameters, - onDefineScopedParameters: this.onDefineScopedParameters.bind(this), - onExecute: this.onExecute.bind(this) + onDefineScopedParameters: this.onDefineScopedParameters.bind(this) }); } @@ -153,6 +149,7 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { // Call the scoped parser using only the scoped args. await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); + await super._execute(); return; } From 90afc1881d5cdbdaf7fe342399ac474a0ab4c0fa Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 6 May 2022 18:23:45 -0700 Subject: [PATCH 13/16] Break a circular reference. --- libraries/ts-command-line/src/Constants.ts | 2 ++ libraries/ts-command-line/src/parameters/BaseClasses.ts | 2 +- .../ts-command-line/src/parameters/CommandLineDefinition.ts | 2 +- .../src/providers/CommandLineParameterProvider.ts | 2 +- .../ts-command-line/src/providers/ScopedCommandLineAction.ts | 3 +-- 5 files changed, 6 insertions(+), 5 deletions(-) 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/parameters/BaseClasses.ts b/libraries/ts-command-line/src/parameters/BaseClasses.ts index 95a1e2c3fb0..1950696fae6 100644 --- a/libraries/ts-command-line/src/parameters/BaseClasses.ts +++ b/libraries/ts-command-line/src/parameters/BaseClasses.ts @@ -1,7 +1,7 @@ // 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 '../providers/ScopedCommandLineAction'; +import type { SCOPING_PARAMETER_GROUP } from '../Constants'; import { IBaseCommandLineDefinition, IBaseCommandLineDefinitionWithArgument } from './CommandLineDefinition'; /** diff --git a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts index 68af29cfb61..a5322d02708 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineDefinition.ts @@ -1,7 +1,7 @@ // 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 '../providers/ScopedCommandLineAction'; +import type { SCOPING_PARAMETER_GROUP } from '../Constants'; /** * For use with CommandLineParser, this interface represents a generic command-line parameter diff --git a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts index 6e5dabbabf5..7e02763190b 100644 --- a/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts +++ b/libraries/ts-command-line/src/providers/CommandLineParameterProvider.ts @@ -27,7 +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 './ScopedCommandLineAction'; +import { SCOPING_PARAMETER_GROUP } from '../Constants'; /** * This is the argparse result data object diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index d6de9f2e15f..bdc10a516f8 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -6,6 +6,7 @@ import { CommandLineParser, ICommandLineParserOptions } from './CommandLineParse 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; @@ -45,8 +46,6 @@ class InternalScopedCommandLineParser extends CommandLineParser { } } -export const SCOPING_PARAMETER_GROUP: unique symbol = Symbol('scoping'); - /** * Represents a sub-command that is part of the CommandLineParser command-line. * Applications should create subclasses of ScopedCommandLineAction corresponding to From de2ec501ca13b4d837f8570c88dd48d4443aef91 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 19:53:19 -0700 Subject: [PATCH 14/16] Fixes for writing help and continuing to execute --- .../src/providers/ScopedCommandLineAction.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index bdc10a516f8..e725b139b5e 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -19,8 +19,13 @@ interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOpti * 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. @@ -37,6 +42,7 @@ class InternalScopedCommandLineParser extends CommandLineParser { }; super(scopedCommandLineParserOptions); + this._canExecute = false; this._internalOptions = options; this._internalOptions.onDefineScopedParameters(this); } @@ -148,9 +154,15 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { scopedArgs.push(...this.remainder.values.slice(1)); } - // Call the scoped parser using only the scoped args. + // Call the scoped parser using only the scoped args to handle parsing await this._scopedCommandLineParser.executeWithoutErrorHandling(scopedArgs); - await super._execute(); + + // 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; } From c01914a5d9e682af6d75a65366b40af66aec66cb Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 19:54:50 -0700 Subject: [PATCH 15/16] Add missing onExecute --- .../src/providers/ScopedCommandLineAction.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index e725b139b5e..c405b4c1825 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -50,6 +50,13 @@ class InternalScopedCommandLineParser extends CommandLineParser { 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; + } } /** From 01c9ca65ff2c3ded90ffffdc5fdbb86df504a5eb Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Fri, 6 May 2022 19:55:43 -0700 Subject: [PATCH 16/16] Nit: import order --- .../ts-command-line/src/providers/ScopedCommandLineAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index c405b4c1825..20c67323f41 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -1,12 +1,12 @@ // 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'; -import { SCOPING_PARAMETER_GROUP } from '../Constants'; interface IInternalScopedCommandLineParserOptions extends ICommandLineParserOptions { readonly actionOptions: ICommandLineActionOptions;