diff --git a/common/changes/@microsoft/rush/feature-add-remainder-args_2025-10-08-23-54.json b/common/changes/@microsoft/rush/feature-add-remainder-args_2025-10-08-23-54.json new file mode 100644 index 00000000000..b84438cf259 --- /dev/null +++ b/common/changes/@microsoft/rush/feature-add-remainder-args_2025-10-08-23-54.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for remainder arguments with new `allowRemainderArguments` option in a global, bulk, and phased command configurations in `common/config/rush/command-line.json`", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/hashed-folder-copy-plugin/feature-add-remainder-args_2025-10-08-23-54.json b/common/changes/@rushstack/hashed-folder-copy-plugin/feature-add-remainder-args_2025-10-08-23-54.json new file mode 100644 index 00000000000..e8c42a34e96 --- /dev/null +++ b/common/changes/@rushstack/hashed-folder-copy-plugin/feature-add-remainder-args_2025-10-08-23-54.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/hashed-folder-copy-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/hashed-folder-copy-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/ts-command-line/feature-add-remainder-args_2025-10-16-14-00.json b/common/changes/@rushstack/ts-command-line/feature-add-remainder-args_2025-10-16-14-00.json new file mode 100644 index 00000000000..766bb222501 --- /dev/null +++ b/common/changes/@rushstack/ts-command-line/feature-add-remainder-args_2025-10-16-14-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/ts-command-line", + "comment": "This change introduces enhanced support for remainder arguments in the CommandLineRemainder class. The -- separator used to delimit remainder arguments is now automatically excluded from the values array. The -- separator used to delimit remainder arguments is now automatically excluded from the values array. For example, my-tool --flag -- arg1 arg2 will result in values being [\"arg1\", \"arg2\"], not [\"--\", \"arg1\", \"arg2\"].", + "type": "patch" + } + ], + "packageName": "@rushstack/ts-command-line" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 3fa9d1b178b..f8be6666e51 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -430,6 +430,7 @@ export interface ICreateOperationsContext { readonly projectConfigurations: ReadonlyMap; readonly projectSelection: ReadonlySet; readonly projectsInUnknownState: ReadonlySet; + readonly remainderArgs?: ReadonlyArray; readonly rushConfiguration: RushConfiguration; } diff --git a/libraries/rush-lib/src/api/CommandLineJson.ts b/libraries/rush-lib/src/api/CommandLineJson.ts index e6507e49633..69b28c79b26 100644 --- a/libraries/rush-lib/src/api/CommandLineJson.ts +++ b/libraries/rush-lib/src/api/CommandLineJson.ts @@ -30,6 +30,7 @@ export interface IBulkCommandJson extends IBaseCommandJson { allowWarningsInSuccessfulBuild?: boolean; watchForChanges?: boolean; disableBuildCache?: boolean; + allowRemainderArguments?: boolean; } /** @@ -41,6 +42,7 @@ export interface IPhasedCommandWithoutPhasesJson extends IBaseCommandJson { enableParallelism: boolean; allowOversubscription?: boolean; incremental?: boolean; + allowRemainderArguments?: boolean; } /** @@ -64,6 +66,7 @@ export interface IPhasedCommandJson extends IPhasedCommandWithoutPhasesJson { export interface IGlobalCommandJson extends IBaseCommandJson { commandKind: 'global'; shellCommand: string; + allowRemainderArguments?: boolean; } export type CommandJson = IBulkCommandJson | IGlobalCommandJson | IPhasedCommandJson; diff --git a/libraries/rush-lib/src/api/test/CommandLineConfiguration.test.ts b/libraries/rush-lib/src/api/test/CommandLineConfiguration.test.ts index 6cd9360ed1b..b9e80f32460 100644 --- a/libraries/rush-lib/src/api/test/CommandLineConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/CommandLineConfiguration.test.ts @@ -294,4 +294,129 @@ describe(CommandLineConfiguration.name, () => { expect(phase.shellCommand).toEqual('echo'); }); }); + + describe('allowRemainderArguments configuration', () => { + it('should accept allowRemainderArguments for bulk commands', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'bulk', + name: 'test-remainder-bulk', + summary: 'Test bulk command with remainder arguments', + enableParallelism: true, + safeForSimultaneousRushProcesses: false, + allowRemainderArguments: true + } + ] + }); + + const command = commandLineConfiguration.commands.get('test-remainder-bulk'); + expect(command).toBeDefined(); + expect(command?.allowRemainderArguments).toBe(true); + }); + + it('should accept allowRemainderArguments for global commands', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'global', + name: 'test-remainder-global', + summary: 'Test global command with remainder arguments', + shellCommand: 'echo', + safeForSimultaneousRushProcesses: false, + allowRemainderArguments: true + } + ] + }); + + const command = commandLineConfiguration.commands.get('test-remainder-global'); + expect(command).toBeDefined(); + expect(command?.allowRemainderArguments).toBe(true); + }); + + it('should accept allowRemainderArguments for phased commands', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'phased', + name: 'test-remainder-phased', + summary: 'Test phased command with remainder arguments', + enableParallelism: true, + safeForSimultaneousRushProcesses: false, + phases: ['_phase:test'], + allowRemainderArguments: true + } + ], + phases: [ + { + name: '_phase:test' + } + ] + }); + + const command = commandLineConfiguration.commands.get('test-remainder-phased'); + expect(command).toBeDefined(); + expect(command?.allowRemainderArguments).toBe(true); + }); + + it('should default allowRemainderArguments to false when not specified', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'global', + name: 'test-no-remainder', + summary: 'Test command without remainder arguments', + shellCommand: 'echo', + safeForSimultaneousRushProcesses: false + } + ] + }); + + const command = commandLineConfiguration.commands.get('test-no-remainder'); + expect(command).toBeDefined(); + expect(command?.allowRemainderArguments).toBeUndefined(); + }); + + it('should work with both custom parameters and remainder arguments', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'global', + name: 'test-mixed-params', + summary: 'Test command with both custom parameters and remainder arguments', + shellCommand: 'echo', + safeForSimultaneousRushProcesses: false, + allowRemainderArguments: true + } + ], + parameters: [ + { + parameterKind: 'flag', + longName: '--verbose', + associatedCommands: ['test-mixed-params'], + description: 'Enable verbose logging' + }, + { + parameterKind: 'string', + longName: '--output', + argumentName: 'PATH', + associatedCommands: ['test-mixed-params'], + description: 'Output file path' + }, + { + parameterKind: 'integer', + longName: '--count', + argumentName: 'NUM', + associatedCommands: ['test-mixed-params'], + description: 'Number of iterations' + } + ] + }); + + const command = commandLineConfiguration.commands.get('test-mixed-params'); + expect(command).toBeDefined(); + expect(command?.allowRemainderArguments).toBe(true); + expect(command?.associatedParameters.size).toBe(3); + }); + }); }); diff --git a/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts index 2da245a5b7a..3097b2bd64b 100644 --- a/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts @@ -42,6 +42,14 @@ export abstract class BaseScriptAction extends BaseRus return; } + // Define remainder parameter if the command allows it + if (this.command.allowRemainderArguments) { + this.defineCommandLineRemainder({ + description: + 'Additional command-line arguments to be passed through to the shell command or npm script' + }); + } + // Find any parameters that are associated with this command for (const parameter of this.command.associatedParameters) { let tsCommandLineParameter: CommandLineParameter | undefined; diff --git a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index 9b35156f188..7cd6996759c 100644 --- a/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -135,6 +135,11 @@ export class GlobalScriptAction extends BaseScriptAction { tsCommandLineParameter.appendToArgList(customParameterValues); } + // Add remainder arguments if they exist + if (this.remainder) { + this.remainder.appendToArgList(customParameterValues); + } + for (let i: number = 0; i < customParameterValues.length; i++) { let customParameterValue: string = customParameterValues[i]; customParameterValue = customParameterValue.replace(/"/g, '\\"'); diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 906a3ca89ea..e7277ae7d5c 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -566,6 +566,7 @@ export class PhasedScriptAction extends BaseScriptAction i changedProjectsOnly, cobuildConfiguration, customParameters: customParametersByName, + remainderArgs: this.remainder?.values, isIncrementalBuildAllowed: this._isIncrementalBuildAllowed, isInitial: true, isWatch, diff --git a/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts index 550cc726889..b980c6406ce 100644 --- a/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts @@ -38,7 +38,7 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { before: ShellOperationPluginName }, async (operations: Set, context: ICreateOperationsContext) => { - const { isWatch, isInitial } = context; + const { isWatch, isInitial, remainderArgs } = context; if (!isWatch) { return operations; } @@ -46,7 +46,7 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { currentContext = context; const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); + getCustomParameterValuesByPhase(remainderArgs); for (const operation of operations) { const { associatedPhase: phase, associatedProject: project, runner } = operation; diff --git a/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts b/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts index 20a0ce44dbd..6f82c803d8f 100644 --- a/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts @@ -44,10 +44,10 @@ export class ShardedPhasedOperationPlugin implements IPhasedCommandPlugin { } function spliceShards(existingOperations: Set, context: ICreateOperationsContext): Set { - const { rushConfiguration, projectConfigurations } = context; + const { rushConfiguration, projectConfigurations, remainderArgs } = context; const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); + getCustomParameterValuesByPhase(remainderArgs); for (const operation of existingOperations) { const { diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts index 96186d9e0d8..c0d4a7ef44b 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts @@ -29,10 +29,10 @@ export class ShellOperationRunnerPlugin implements IPhasedCommandPlugin { operations: Set, context: ICreateOperationsContext ): Set { - const { rushConfiguration, isInitial } = context; + const { rushConfiguration, isInitial, remainderArgs } = context; const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); + getCustomParameterValuesByPhase(remainderArgs); for (const operation of operations) { const { associatedPhase: phase, associatedProject: project } = operation; @@ -120,7 +120,9 @@ export function initializeShellOperationRunner(options: { * Memoizer for custom parameter values by phase * @returns A function that returns the custom parameter values for a given phase */ -export function getCustomParameterValuesByPhase(): (phase: IPhase) => ReadonlyArray { +export function getCustomParameterValuesByPhase( + remainderArgs?: ReadonlyArray +): (phase: IPhase) => ReadonlyArray { const customParametersByPhase: Map = new Map(); function getCustomParameterValuesForPhase(phase: IPhase): ReadonlyArray { @@ -131,6 +133,11 @@ export function getCustomParameterValuesByPhase(): (phase: IPhase) => ReadonlyAr tsCommandLineParameter.appendToArgList(customParameterValues); } + // Add remainder arguments if they exist + if (remainderArgs && remainderArgs.length > 0) { + customParameterValues.push(...remainderArgs); + } + customParametersByPhase.set(phase, customParameterValues); } diff --git a/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts b/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts index 912531e3eef..e38b387f627 100644 --- a/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts +++ b/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts @@ -121,4 +121,63 @@ describe(ShellOperationRunnerPlugin.name, () => { // All projects expect(Array.from(operations, serializeOperation)).toMatchSnapshot(); }); + + it('should handle remainderArgs when provided in context', async () => { + const rushJsonFile: string = path.resolve(__dirname, `../../test/customShellCommandinBulkRepo/rush.json`); + const commandLineJsonFile: string = path.resolve( + __dirname, + `../../test/customShellCommandinBulkRepo/common/config/rush/command-line.json` + ); + + const rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + const commandLineJson: ICommandLineJson = JsonFile.load(commandLineJsonFile); + + const commandLineConfiguration = new CommandLineConfiguration(commandLineJson); + + const echoCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get( + 'echo' + )! as IPhasedCommandConfig; + + // Create context with remainder arguments + const fakeCreateOperationsContext: Pick< + ICreateOperationsContext, + | 'phaseOriginal' + | 'phaseSelection' + | 'projectSelection' + | 'projectsInUnknownState' + | 'projectConfigurations' + | 'remainderArgs' + > = { + phaseOriginal: echoCommand.phases, + phaseSelection: echoCommand.phases, + projectSelection: new Set(rushConfiguration.projects), + projectsInUnknownState: new Set(rushConfiguration.projects), + projectConfigurations: new Map(), + remainderArgs: ['--verbose', '--output', 'file.log'] + }; + + const hooks: PhasedCommandHooks = new PhasedCommandHooks(); + + // Generates the default operation graph + new PhasedOperationPlugin().apply(hooks); + // Applies the Shell Operation Runner to selected operations + new ShellOperationRunnerPlugin().apply(hooks); + + const operations: Set = await hooks.createOperations.promise( + new Set(), + fakeCreateOperationsContext as ICreateOperationsContext + ); + + // Verify that operations were created and include remainder args in config hash + expect(operations.size).toBeGreaterThan(0); + + // Get the first operation and check that remainder args affect the command configuration + const operation = Array.from(operations)[0]; + const configHash = operation.runner!.getConfigHash(); + + // The config hash should include the remainder arguments + expect(configHash).toContain('--verbose'); + expect(configHash).toContain('--output'); + expect(configHash).toContain('file.log'); + }); }); diff --git a/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts b/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts index 97ccdb064aa..a29c7d4685d 100644 --- a/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts +++ b/libraries/rush-lib/src/pluginFramework/PhasedCommandHooks.ts @@ -64,6 +64,11 @@ export interface ICreateOperationsContext { * Maps from the `longName` field in command-line.json to the parser configuration in ts-command-line. */ readonly customParameters: ReadonlyMap; + /** + * The remainder arguments from the command line, if any. + * These are additional arguments that were not recognized as regular parameters. + */ + readonly remainderArgs?: ReadonlyArray; /** * If true, projects may read their output from cache or be skipped if already up to date. * If false, neither of the above may occur, e.g. "rush rebuild" diff --git a/libraries/rush-lib/src/schemas/command-line.schema.json b/libraries/rush-lib/src/schemas/command-line.schema.json index 0091e7bb7ae..84b7da939bc 100644 --- a/libraries/rush-lib/src/schemas/command-line.schema.json +++ b/libraries/rush-lib/src/schemas/command-line.schema.json @@ -100,6 +100,11 @@ "title": "Disable build cache.", "description": "Disable build cache for this action. This may be useful if this command affects state outside of projects' own folders. If the build cache is not configured, this also disables the legacy skip detection logic.", "type": "boolean" + }, + "allowRemainderArguments": { + "title": "Allow Remainder Arguments", + "description": "If true, this command will accept additional command-line arguments that appear after the \"--\" separator. Everything after \"--\" (not including the \"--\" itself) will be passed through to the shell command or npm script. Any unrecognized arguments before \"--\" will still cause an error. Example: \"rush my-command --known-flag value -- --arbitrary --args here\" will pass \"--arbitrary --args here\" to the script, but \"rush my-command --unknown-flag\" will fail with an error about the unrecognized parameter.", + "type": "boolean" } } }, @@ -121,7 +126,8 @@ "incremental": { "$ref": "#/definitions/anything" }, "allowWarningsInSuccessfulBuild": { "$ref": "#/definitions/anything" }, "watchForChanges": { "$ref": "#/definitions/anything" }, - "disableBuildCache": { "$ref": "#/definitions/anything" } + "disableBuildCache": { "$ref": "#/definitions/anything" }, + "allowRemainderArguments": { "$ref": "#/definitions/anything" } } } ] @@ -149,6 +155,11 @@ "title": "Autoinstaller Name", "description": "If your \"shellCommand\" script depends on NPM packages, the recommended best practice is to make it into a regular Rush project that builds using your normal toolchain. In cases where the command needs to work without first having to run \"rush build\", the recommended practice is to publish the project to an NPM registry and use common/scripts/install-run.js to launch it.\n\nAutoinstallers offer another possibility: They are folders under \"common/autoinstallers\" with a package.json file and shrinkwrap file. Rush will automatically invoke the package manager to install these dependencies before an associated command is invoked. Autoinstallers have the advantage that they work even in a branch where \"rush install\" is broken, which makes them a good solution for Git hook scripts. But they have the disadvantages of not being buildable projects, and of increasing the overall installation footprint for your monorepo.\n\nThe \"autoinstallerName\" setting must not contain a path and must be a valid NPM package name.\n\nFor example, the name \"my-task\" would map to \"common/autoinstallers/my-task/package.json\", and the \"common/autoinstallers/my-task/node_modules/.bin\" folder would be added to the shell PATH when invoking the \"shellCommand\".", "type": "string" + }, + "allowRemainderArguments": { + "title": "Allow Remainder Arguments", + "description": "If true, this command will accept additional command-line arguments that appear after the \"--\" separator. Everything after \"--\" (not including the \"--\" itself) will be passed through to the shell command. Any unrecognized arguments before \"--\" will still cause an error. Example: \"rush my-command --known-flag value -- --arbitrary --args here\" will pass \"--arbitrary --args here\" to the script, but \"rush my-command --unknown-flag\" will fail with an error about the unrecognized parameter.", + "type": "boolean" } } }, @@ -163,7 +174,8 @@ "safeForSimultaneousRushProcesses": { "$ref": "#/definitions/anything" }, "shellCommand": { "$ref": "#/definitions/anything" }, - "autoinstallerName": { "$ref": "#/definitions/anything" } + "autoinstallerName": { "$ref": "#/definitions/anything" }, + "allowRemainderArguments": { "$ref": "#/definitions/anything" } } } ] @@ -250,6 +262,11 @@ "type": "boolean" } } + }, + "allowRemainderArguments": { + "title": "Allow Remainder Arguments", + "description": "If true, this command will accept additional command-line arguments that appear after the \"--\" separator. Everything after \"--\" (not including the \"--\" itself) will be passed through to the phase scripts. Any unrecognized arguments before \"--\" will still cause an error. Example: \"rush my-command --known-flag value -- --arbitrary --args here\" will pass \"--arbitrary --args here\" to the phase scripts, but \"rush my-command --unknown-flag\" will fail with an error about the unrecognized parameter.", + "type": "boolean" } } }, @@ -268,7 +285,8 @@ "incremental": { "$ref": "#/definitions/anything" }, "phases": { "$ref": "#/definitions/anything" }, "watchOptions": { "$ref": "#/definitions/anything" }, - "installOptions": { "$ref": "#/definitions/anything" } + "installOptions": { "$ref": "#/definitions/anything" }, + "allowRemainderArguments": { "$ref": "#/definitions/anything" } } } ] diff --git a/libraries/ts-command-line/src/parameters/CommandLineRemainder.ts b/libraries/ts-command-line/src/parameters/CommandLineRemainder.ts index 06e1106632a..395fa344529 100644 --- a/libraries/ts-command-line/src/parameters/CommandLineRemainder.ts +++ b/libraries/ts-command-line/src/parameters/CommandLineRemainder.ts @@ -24,6 +24,10 @@ export class CommandLineRemainder { * * @remarks * The array will be empty if the command-line has not been parsed yet. + * + * When the `--` separator is used to delimit remainder arguments, it is automatically + * excluded from this array. For example, `my-tool --flag -- arg1 arg2` will result in + * `values` being `["arg1", "arg2"]`, not `["--", "arg1", "arg2"]`. */ public get values(): ReadonlyArray { return this._values; @@ -39,7 +43,10 @@ export class CommandLineRemainder { throw new Error(`Unexpected data object for remainder: ` + JSON.stringify(data)); } - this._values.push(...data); + // Filter out the '--' separator that argparse includes in the remainder values. + // Users expect everything AFTER '--' to be passed through, not including '--' itself. + const filteredData: string[] = data.filter((value: string) => value !== '--'); + this._values.push(...filteredData); } /** {@inheritDoc CommandLineParameterBase.appendToArgList} @override */ diff --git a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts index a75d582142f..7c533f6ea04 100644 --- a/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts +++ b/libraries/ts-command-line/src/providers/ScopedCommandLineAction.ts @@ -4,7 +4,6 @@ import { SCOPING_PARAMETER_GROUP } from '../Constants'; import { CommandLineAction, type ICommandLineActionOptions } from './CommandLineAction'; import { CommandLineParser, type ICommandLineParserOptions } from './CommandLineParser'; -import { CommandLineParserExitError } from './CommandLineParserExitError'; import type { CommandLineParameter } from '../parameters/BaseClasses'; import type { CommandLineParameterProvider, @@ -180,23 +179,9 @@ export abstract class ScopedCommandLineAction extends CommandLineAction { throw new Error('Parameters must be defined 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] !== '--') { - throw new CommandLineParserExitError( - // argparse sets exit code 2 for invalid arguments - 2, - // model the message off of the built-in "unrecognized arguments" message - `${this.renderUsageText()}\n${this._unscopedParserOptions.toolFilename} ${this.actionName}: ` + - `error: Unrecognized arguments: ${this.remainder.values[0]}.\n` - ); - } - for (const scopedArg of this.remainder.values.slice(1)) { - scopedArgs.push(scopedArg); - } - } + // The remainder values now have the '--' separator already filtered out by CommandLineRemainder._setValue(). + // All values in remainder are scoped arguments that should be passed to the scoped parser. + const scopedArgs: string[] = [...this.remainder.values]; // Call the scoped parser using only the scoped args to handle parsing await this._scopedCommandLineParser.executeWithoutErrorHandlingAsync(scopedArgs); diff --git a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts b/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts deleted file mode 100644 index e6d08dabe2a..00000000000 --- a/libraries/ts-command-line/src/test/CommandLineRemainder.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import type { CommandLineAction } from '../providers/CommandLineAction'; -import type { CommandLineParser } from '../providers/CommandLineParser'; -import { DynamicCommandLineParser } from '../providers/DynamicCommandLineParser'; -import { DynamicCommandLineAction } from '../providers/DynamicCommandLineAction'; -import { CommandLineRemainder } from '../parameters/CommandLineRemainder'; -import { ensureHelpTextMatchesSnapshot } from './helpTestUtilities'; - -function createParser(): DynamicCommandLineParser { - const commandLineParser: DynamicCommandLineParser = new DynamicCommandLineParser({ - toolFilename: 'example', - toolDescription: 'An example project' - }); - commandLineParser.defineFlagParameter({ - parameterLongName: '--verbose', - description: 'A flag that affects all actions' - }); - - const action: DynamicCommandLineAction = new DynamicCommandLineAction({ - actionName: 'run', - summary: 'does the job', - documentation: 'a longer description' - }); - commandLineParser.addAction(action); - - action.defineStringParameter({ - parameterLongName: '--title', - description: 'A string', - argumentName: 'TEXT' - }); - - // Although this is defined BEFORE the parameter, but it should still capture the end - action.defineCommandLineRemainder({ - description: 'The action remainder' - }); - - commandLineParser._registerDefinedParameters({ parentParameterNames: new Set() }); - - return commandLineParser; -} - -describe(CommandLineRemainder.name, () => { - it('renders help text', () => { - const commandLineParser: CommandLineParser = createParser(); - ensureHelpTextMatchesSnapshot(commandLineParser); - }); - - it('parses an action input with remainder', async () => { - const commandLineParser: CommandLineParser = createParser(); - const action: CommandLineAction = commandLineParser.getAction('run'); - const args: string[] = ['run', '--title', 'The title', 'the', 'remaining', 'args']; - - await commandLineParser.executeAsync(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(); - }); - - 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.executeAsync(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__/CommandLineRemainder.test.ts.snap b/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap deleted file mode 100644 index 9205cbc2f2f..00000000000 --- a/libraries/ts-command-line/src/test/__snapshots__/CommandLineRemainder.test.ts.snap +++ /dev/null @@ -1,57 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CommandLineRemainder parses an action input with remainder 1`] = ` -Array [ - "### --title output: ###", - "--title", - "The title", - "### remainder output: ###", - "the", - "remaining", - "args", -] -`; - -exports[`CommandLineRemainder parses an action input with remainder flagged options 1`] = ` -Array [ - "### --title output: ###", - "--title", - "The title", - "### remainder output: ###", - "--", - "--the", - "remaining", - "--args", -] -`; - -exports[`CommandLineRemainder renders help text: global help 1`] = ` -"usage: example [-h] [--verbose] ... - -An example project - -Positional arguments: - - run does the job - -Optional arguments: - -h, --help Show this help message and exit. - --verbose A flag that affects all actions - -[bold]For detailed help about a specific command, use: example -h[normal] -" -`; - -exports[`CommandLineRemainder renders help text: run 1`] = ` -"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 -" -`; 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 30828b18fb2..985a8ffedff 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 @@ -93,9 +93,7 @@ Optional scoping arguments: `; exports[`CommandLineParser throws on missing positional arg divider with unknown positional args 1`] = ` -"usage: example scoped-action [-h] [--verbose] [--scope SCOPE] ... - -example scoped-action: error: Unrecognized arguments: bar. +"example scoped-action --scope foo --: error: Unrecognized arguments: bar. " `;