Skip to content

Commit

Permalink
Merge pull request #3364 from D4N14L/user/danade/ScopedCommandLine
Browse files Browse the repository at this point in the history
[ts-command-line] Add ScopedCommandLineAction class
  • Loading branch information
iclanton authored May 10, 2022
2 parents 8887475 + 01c9ca6 commit fd8c362
Show file tree
Hide file tree
Showing 16 changed files with 664 additions and 48 deletions.
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
6 changes: 4 additions & 2 deletions libraries/ts-command-line/src/providers/CommandLineAction.ts
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

0 comments on commit fd8c362

Please sign in to comment.