diff --git a/Readme.md b/Readme.md index 248bcf2d0..987e920d7 100644 --- a/Readme.md +++ b/Readme.md @@ -488,10 +488,12 @@ program #### Custom argument processing -You may specify a function to do custom processing of command-arguments before they are passed to the action handler. +You may specify a function to do custom processing of command-arguments (like for option-arguments). The callback function receives two parameters, the user specified command-argument and the previous value for the argument. It returns the new value for the argument. +The processed argument values are passed to the action handler, and saved as `.processedArgs`. + You can optionally specify the default/starting value for the argument after the function parameter. Example file: [arguments-custom-processing.js](./examples/arguments-custom-processing.js) diff --git a/lib/command.js b/lib/command.js index 175213ae1..4cba027e3 100644 --- a/lib/command.js +++ b/lib/command.js @@ -19,13 +19,19 @@ class Command extends EventEmitter { constructor(name) { super(); + /** @type {Command[]} */ this.commands = []; + /** @type {Option[]} */ this.options = []; this.parent = null; this._allowUnknownOption = false; this._allowExcessArguments = true; + /** @type {Argument[]} */ this._args = []; - this.rawArgs = null; + /** @type {string[]} */ + this.args = []; // cli args with options removed + this.rawArgs = []; + this.processedArgs = []; // like .args but after custom processing and collecting variadic this._scriptPath = null; this._name = name || ''; this._optionValues = {}; @@ -976,13 +982,34 @@ Expecting one of '${allowedValues.join("', '")}'`); }; /** - * Package arguments (this.args) for passing to action handler based - * on declared arguments (this._args). + * Check this.args against expected this._args. * * @api private */ - _getActionArguments() { + _checkNumberOfArguments() { + // too few + this._args.forEach((arg, i) => { + if (arg.required && this.args[i] == null) { + this.missingArgument(arg.name()); + } + }); + // too many + if (this._args.length > 0 && this._args[this._args.length - 1].variadic) { + return; + } + if (this.args.length > this._args.length) { + this._excessArguments(this.args); + } + }; + + /** + * Process this.args using this._args and save as this.processedArgs! + * + * @api private + */ + + _processArguments() { const myParseArg = (argument, value, previous) => { // Extra processing for nice error message on parsing failure. let parsedValue = value; @@ -1000,7 +1027,9 @@ Expecting one of '${allowedValues.join("', '")}'`); return parsedValue; }; - const actionArgs = []; + this._checkNumberOfArguments(); + + const processedArgs = []; this._args.forEach((declaredArg, index) => { let value = declaredArg.defaultValue; if (declaredArg.variadic) { @@ -1021,9 +1050,9 @@ Expecting one of '${allowedValues.join("', '")}'`); value = myParseArg(declaredArg, value, declaredArg.defaultValue); } } - actionArgs[index] = value; + processedArgs[index] = value; }); - return actionArgs; + this.processedArgs = processedArgs; } /** @@ -1115,37 +1144,22 @@ Expecting one of '${allowedValues.join("', '")}'`); this.unknownOption(parsed.unknown[0]); } }; - const checkNumberOfArguments = () => { - // too few - this._args.forEach((arg, i) => { - if (arg.required && this.args[i] == null) { - this.missingArgument(arg.name()); - } - }); - // too many - if (this._args.length > 0 && this._args[this._args.length - 1].variadic) { - return; - } - if (this.args.length > this._args.length) { - this._excessArguments(this.args); - } - }; const commandEvent = `command:${this.name()}`; if (this._actionHandler) { checkForUnknownOptions(); - checkNumberOfArguments(); + this._processArguments(); let actionResult; actionResult = this._chainOrCallHooks(actionResult, 'preAction'); - actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this._getActionArguments())); + actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs)); if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy actionResult = this._chainOrCallHooks(actionResult, 'postAction'); return actionResult; } if (this.parent && this.parent.listenerCount(commandEvent)) { checkForUnknownOptions(); - checkNumberOfArguments(); + this._processArguments(); this.parent.emit(commandEvent, operands, unknown); // legacy } else if (operands.length) { if (this._findCommand('*')) { // legacy default command @@ -1158,14 +1172,14 @@ Expecting one of '${allowedValues.join("', '")}'`); this.unknownCommand(); } else { checkForUnknownOptions(); - checkNumberOfArguments(); + this._processArguments(); } } else if (this.commands.length) { // This command has subcommands and nothing hooked up at this level, so display help (and exit). this.help({ error: true }); } else { checkForUnknownOptions(); - checkNumberOfArguments(); + this._processArguments(); // fall through for caller to handle after calling .parse() } }; @@ -1513,6 +1527,7 @@ Expecting one of '${allowedValues.join("', '")}'`); alias(alias) { if (alias === undefined) return this._aliases[0]; // just return first, for backwards compatibility + /** @type {Command} */ let command = this; if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) { // assume adding alias for last added executable subcommand, rather than this diff --git a/lib/help.js b/lib/help.js index 61895258f..025bc0743 100644 --- a/lib/help.js +++ b/lib/help.js @@ -38,6 +38,7 @@ class Help { } if (this.sortSubcommands) { visibleCommands.sort((a, b) => { + // @ts-ignore: overloaded return type return a.name().localeCompare(b.name()); }); } diff --git a/tests/argument.custom-processing.test.js b/tests/argument.custom-processing.test.js index bbc11b864..ffea759a8 100644 --- a/tests/argument.custom-processing.test.js +++ b/tests/argument.custom-processing.test.js @@ -1,6 +1,7 @@ const commander = require('../'); // Testing default value and custom processing behaviours. +// Some double assertions in tests to check action argument and .processedArg test('when argument not specified then callback not called', () => { const mockCoercion = jest.fn(); @@ -34,7 +35,7 @@ test('when custom with starting value and argument not specified then callback n expect(mockCoercion).not.toHaveBeenCalled(); }); -test('when custom with starting value and argument not specified then action argument is starting value', () => { +test('when custom with starting value and argument not specified with action handler then action argument is starting value', () => { const startingValue = 1; let actionValue; const program = new commander.Command(); @@ -45,9 +46,19 @@ test('when custom with starting value and argument not specified then action arg }); program.parse([], { from: 'user' }); expect(actionValue).toEqual(startingValue); + expect(program.processedArgs).toEqual([startingValue]); }); -test('when default value is defined (without custom processing) and argument not specified then action argument is default value', () => { +test('when custom with starting value and argument not specified without action handler then .processedArgs has starting value', () => { + const startingValue = 1; + const program = new commander.Command(); + program + .argument('[n]', 'number', parseFloat, startingValue); + program.parse([], { from: 'user' }); + expect(program.processedArgs).toEqual([startingValue]); +}); + +test('when default value is defined (without custom processing) and argument not specified with action handler then action argument is default value', () => { const defaultValue = 1; let actionValue; const program = new commander.Command(); @@ -58,6 +69,16 @@ test('when default value is defined (without custom processing) and argument not }); program.parse([], { from: 'user' }); expect(actionValue).toEqual(defaultValue); + expect(program.processedArgs).toEqual([defaultValue]); +}); + +test('when default value is defined (without custom processing) and argument not specified without action handler then .processedArgs is default value', () => { + const defaultValue = 1; + const program = new commander.Command(); + program + .argument('[n]', 'number', defaultValue); + program.parse([], { from: 'user' }); + expect(program.processedArgs).toEqual([defaultValue]); }); test('when argument specified then callback called with value', () => { @@ -71,7 +92,7 @@ test('when argument specified then callback called with value', () => { expect(mockCoercion).toHaveBeenCalledWith(value, undefined); }); -test('when argument specified then action value is as returned from callback', () => { +test('when argument specified with action handler then action value is as returned from callback', () => { const callbackResult = 2; let actionValue; const program = new commander.Command(); @@ -84,6 +105,18 @@ test('when argument specified then action value is as returned from callback', ( }); program.parse(['node', 'test', 'alpha']); expect(actionValue).toEqual(callbackResult); + expect(program.processedArgs).toEqual([callbackResult]); +}); + +test('when argument specified without action handler then .processedArgs is as returned from callback', () => { + const callbackResult = 2; + const program = new commander.Command(); + program + .argument('[n]', 'number', () => { + return callbackResult; + }); + program.parse(['node', 'test', 'alpha']); + expect(program.processedArgs).toEqual([callbackResult]); }); test('when argument specified then program.args has original rather than custom', () => { @@ -124,6 +157,14 @@ test('when variadic argument specified multiple times then callback called with expect(mockCoercion).toHaveBeenNthCalledWith(2, '2', 'callback'); }); +test('when variadic argument without action handler then .processedArg has array', () => { + const program = new commander.Command(); + program + .argument('', 'number'); + program.parse(['1', '2'], { from: 'user' }); + expect(program.processedArgs).toEqual([['1', '2']]); +}); + test('when parseFloat "1e2" then action argument is 100', () => { let actionValue; const program = new commander.Command(); @@ -134,6 +175,7 @@ test('when parseFloat "1e2" then action argument is 100', () => { }); program.parse(['1e2'], { from: 'user' }); expect(actionValue).toEqual(100); + expect(program.processedArgs).toEqual([actionValue]); }); test('when defined default value for required argument then throw', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index 0814eb050..2d44ba721 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -217,6 +217,7 @@ export interface OptionValues { export class Command { args: string[]; + processedArgs: any[]; commands: Command[]; parent: Command | null; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index bc45289ee..32c2a62ec 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -25,6 +25,8 @@ expectType(commander.createArgument('')); // Command properties expectType(program.args); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +expectType(program.processedArgs); expectType(program.commands); expectType(program.parent);