Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ts-command-line] Add ScopedCommandLineAction class #3364

Merged
merged 19 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@rushstack/ts-command-line",
"comment": "Add ScopedCommandLineAction class, which allows for the definition of actions that have dynamic arguments whose definition depends on a provided scope. See https://github.com/microsoft/rushstack/pull/3364",
"type": "minor"
}
],
"packageName": "@rushstack/ts-command-line"
}
29 changes: 27 additions & 2 deletions common/reviews/api/ts-command-line.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export abstract class CommandLineAction extends CommandLineParameterProvider {
protected abstract onDefineParameters(): void;
protected abstract onExecute(): Promise<void>;
// @internal
_processParsedData(data: _ICommandLineParserData): void;
_processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void;
readonly summary: string;
}

Expand Down Expand Up @@ -115,6 +115,7 @@ export abstract class CommandLineParameter {
_getSupplementaryNotes(supplementaryNotes: string[]): void;
abstract get kind(): CommandLineParameterKind;
readonly longName: string;
readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined;
// @internal
_parserKey: string | undefined;
protected reportInvalidData(data: any): never;
Expand Down Expand Up @@ -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
Expand All @@ -164,9 +167,10 @@ export abstract class CommandLineParameterProvider {
get parameters(): ReadonlyArray<CommandLineParameter>;
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
Expand Down Expand Up @@ -249,6 +253,7 @@ export class DynamicCommandLineParser extends CommandLineParser {
export interface IBaseCommandLineDefinition {
description: string;
environmentVariable?: string;
parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP;
parameterLongName: string;
parameterShortName?: string;
required?: boolean;
Expand Down Expand Up @@ -306,6 +311,7 @@ export interface _ICommandLineParserData {
export interface ICommandLineParserOptions {
enableTabCompletionAction?: boolean;
toolDescription: string;
toolEpilog?: string;
toolFilename: string;
}

Expand All @@ -323,4 +329,23 @@ export interface ICommandLineStringDefinition extends IBaseCommandLineDefinition
export interface ICommandLineStringListDefinition extends IBaseCommandLineDefinitionWithArgument {
}

// @public
export abstract class ScopedCommandLineAction extends CommandLineAction {
constructor(options: ICommandLineActionOptions);
// @internal (undocumented)
protected _defineParameter(parameter: CommandLineParameter): void;
// @internal
_execute(): Promise<void>;
// @internal
protected _getScopedCommandLineParser(): CommandLineParser;
protected onDefineParameters(): void;
protected abstract onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void;
protected abstract onDefineUnscopedParameters(): void;
protected abstract onExecute(): Promise<void>;
get parameters(): ReadonlyArray<CommandLineParameter>;
// @internal
_processParsedData(parserOptions: ICommandLineParserOptions, data: _ICommandLineParserData): void;
static readonly ScopingParameterGroup: typeof SCOPING_PARAMETER_GROUP;
}

```
2 changes: 2 additions & 0 deletions libraries/ts-command-line/src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const enum CommandLineConstants {
*/
TabCompletionActionName = 'tab-complete'
}

export const SCOPING_PARAMETER_GROUP: unique symbol = Symbol('scoping');
5 changes: 2 additions & 3 deletions libraries/ts-command-line/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/

export { CommandLineAction, ICommandLineActionOptions } from './providers/CommandLineAction';
export { DynamicCommandLineAction } from './providers/DynamicCommandLineAction';
export { ScopedCommandLineAction } from './providers/ScopedCommandLineAction';

export {
IBaseCommandLineDefinition,
Expand Down Expand Up @@ -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';
Expand Down
5 changes: 5 additions & 0 deletions libraries/ts-command-line/src/parameters/BaseClasses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { SCOPING_PARAMETER_GROUP } from '../Constants';
import { IBaseCommandLineDefinition, IBaseCommandLineDefinitionWithArgument } from './CommandLineDefinition';

/**
Expand Down Expand Up @@ -53,6 +54,9 @@ export abstract class CommandLineParameter {
/** {@inheritDoc IBaseCommandLineDefinition.parameterShortName} */
public readonly shortName: string | undefined;

/** {@inheritDoc IBaseCommandLineDefinition.parameterGroup} */
public readonly parameterGroup: string | typeof SCOPING_PARAMETER_GROUP | undefined;

/** {@inheritDoc IBaseCommandLineDefinition.description} */
public readonly description: string;

Expand All @@ -69,6 +73,7 @@ export abstract class CommandLineParameter {
public constructor(definition: IBaseCommandLineDefinition) {
this.longName = definition.parameterLongName;
this.shortName = definition.parameterShortName;
this.parameterGroup = definition.parameterGroup;
this.description = definition.description;
this.required = !!definition.required;
this.environmentVariable = definition.environmentVariable;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { SCOPING_PARAMETER_GROUP } from '../Constants';

/**
* For use with CommandLineParser, this interface represents a generic command-line parameter
*
Expand All @@ -17,6 +19,11 @@ export interface IBaseCommandLineDefinition {
*/
parameterShortName?: string;

/**
* An optional parameter group name, shown when invoking the tool with "--help"
*/
parameterGroup?: string | typeof SCOPING_PARAMETER_GROUP;

/**
* Documentation for the parameter that will be shown when invoking the tool with "--help"
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// See LICENSE in the project root for license information.

import * as argparse from 'argparse';
import {

import type {
ICommandLineChoiceDefinition,
ICommandLineChoiceListDefinition,
ICommandLineIntegerDefinition,
Expand All @@ -12,6 +13,7 @@ import {
ICommandLineStringListDefinition,
ICommandLineRemainderDefinition
} from '../parameters/CommandLineDefinition';
import type { ICommandLineParserOptions } from './CommandLineParser';
import {
CommandLineParameter,
CommandLineParameterWithArgument,
Expand All @@ -25,6 +27,7 @@ import { CommandLineFlagParameter } from '../parameters/CommandLineFlagParameter
import { CommandLineStringParameter } from '../parameters/CommandLineStringParameter';
import { CommandLineStringListParameter } from '../parameters/CommandLineStringListParameter';
import { CommandLineRemainder } from '../parameters/CommandLineRemainder';
import { SCOPING_PARAMETER_GROUP } from '../Constants';

/**
* This is the argparse result data object
Expand All @@ -46,14 +49,16 @@ export abstract class CommandLineParameterProvider {

private _parameters: CommandLineParameter[];
private _parametersByLongName: Map<string, CommandLineParameter>;
private _parameterGroupsByName: Map<string | typeof SCOPING_PARAMETER_GROUP, argparse.ArgumentGroup>;
private _parametersProcessed: boolean;
private _remainder: CommandLineRemainder | undefined;

/** @internal */
// Third party code should not inherit subclasses or call this constructor
public constructor() {
this._parameters = [];
this._parametersByLongName = new Map<string, CommandLineParameter>();
this._parametersByLongName = new Map();
this._parameterGroupsByName = new Map();
this._parametersProcessed = false;
}

Expand Down Expand Up @@ -207,6 +212,7 @@ export abstract class CommandLineParameterProvider {
public getIntegerListParameter(parameterLongName: string): CommandLineIntegerListParameter {
return this._getParameter(parameterLongName, CommandLineParameterKind.IntegerList);
}

/**
* Defines a command-line parameter whose argument is a single text string.
*
Expand Down Expand Up @@ -297,6 +303,13 @@ export abstract class CommandLineParameterProvider {
return this._getArgumentParser().formatHelp();
}

/**
* Generates the command-line usage text.
*/
public renderUsageText(): string {
return this._getArgumentParser().formatUsage();
}

/**
* Returns a object which maps the long name of each parameter in this.parameters
* to the stringified form of its value. This is useful for logging telemetry, but
Expand Down Expand Up @@ -349,7 +362,7 @@ export abstract class CommandLineParameterProvider {
protected abstract _getArgumentParser(): argparse.ArgumentParser;

/** @internal */
protected _processParsedData(data: ICommandLineParserData): void {
protected _processParsedData(parserOptions: ICommandLineParserOptions, data: ICommandLineParserData): void {
if (this._parametersProcessed) {
throw new Error('Command Line Parser Data was already processed');
}
Expand All @@ -367,28 +380,8 @@ export abstract class CommandLineParameterProvider {
this._parametersProcessed = true;
}

private _generateKey(): string {
return 'key_' + (CommandLineParameterProvider._keyCounter++).toString();
}

private _getParameter<T extends CommandLineParameter>(
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;' +
Expand Down Expand Up @@ -455,10 +448,32 @@ export abstract class CommandLineParameterProvider {
break;
}

const argumentParser: argparse.ArgumentParser = this._getArgumentParser();
argumentParser.addArgument(names, { ...argparseOptions });
let argumentGroup: argparse.ArgumentGroup | undefined;
if (parameter.parameterGroup) {
argumentGroup = this._parameterGroupsByName.get(parameter.parameterGroup);
if (!argumentGroup) {
let parameterGroupName: string;
if (typeof parameter.parameterGroup === 'string') {
parameterGroupName = parameter.parameterGroup;
} else if (parameter.parameterGroup === SCOPING_PARAMETER_GROUP) {
parameterGroupName = 'scoping';
} else {
throw new Error('Unexpected parameter group: ' + parameter.parameterGroup);
}

argumentGroup = this._getArgumentParser().addArgumentGroup({
title: `Optional ${parameterGroupName} arguments`
});
this._parameterGroupsByName.set(parameter.parameterGroup, argumentGroup);
}
} else {
argumentGroup = this._getArgumentParser();
}

argumentGroup.addArgument(names, { ...argparseOptions });

if (parameter.undocumentedSynonyms && parameter.undocumentedSynonyms.length > 0) {
argumentParser.addArgument(parameter.undocumentedSynonyms, {
argumentGroup.addArgument(parameter.undocumentedSynonyms, {
...argparseOptions,
help: argparse.Const.SUPPRESS
});
Expand All @@ -467,4 +482,25 @@ export abstract class CommandLineParameterProvider {
this._parameters.push(parameter);
this._parametersByLongName.set(parameter.longName, parameter);
}

private _generateKey(): string {
return 'key_' + (CommandLineParameterProvider._keyCounter++).toString();
}

private _getParameter<T extends CommandLineParameter>(
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;
}
}
17 changes: 13 additions & 4 deletions libraries/ts-command-line/src/providers/CommandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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} <command> -h`
this._options.toolEpilog ??
`For detailed help about a specific command, use: ${this._options.toolFilename} <command> -h`
)
});

Expand Down Expand Up @@ -194,19 +201,21 @@ export abstract class CommandLineParser extends CommandLineParameterProvider {
// 0=node.exe, 1=script name
args = process.argv.slice(2);
}
if (args.length === 0) {
if (this.actions.length > 0 && args.length === 0) {
// Parsers that use actions should print help when 0 args are provided. Allow
// actionless parsers to continue on zero args.
this._argumentParser.printHelp();
return;
}

const data: ICommandLineParserData = this._argumentParser.parseArgs(args);

this._processParsedData(data);
this._processParsedData(this._options, data);

for (const action of this._actions) {
if (action.actionName === data.action) {
this.selectedAction = action;
action._processParsedData(data);
action._processParsedData(this._options, data);
break;
}
}
Expand Down
Loading