From d38194489aed77d2f8429d5199c99ef30b78ddc0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 22 May 2021 13:24:17 +1200 Subject: [PATCH 1/5] Apply custom argument processing without action handler --- Readme.md | 4 +++- index.js | 68 +++++++++++++++++++++++++++++++------------------------ 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/Readme.md b/Readme.md index 1b43a9613..44acc2314 100644 --- a/Readme.md +++ b/Readme.md @@ -479,7 +479,9 @@ 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. The processed argument values are +passed to the action handler, and saved as `.processedArgs`. + 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. diff --git a/index.js b/index.js index 3294e9f2b..d16a7fe7c 100644 --- a/index.js +++ b/index.js @@ -656,13 +656,15 @@ class Command extends EventEmitter { constructor(name) { super(); - this.commands = []; - this.options = []; + this.commands = []; // array of Command + this.options = []; // array of Option this.parent = null; this._allowUnknownOption = false; this._allowExcessArguments = true; - this._args = []; + this._args = []; // array of Argument + this.args = undefined; // cli args with options removed this.rawArgs = null; + this.processedArgs = null; // like .args but after custom processing and collecting variadic this._scriptPath = null; this._name = name || ''; this._optionValues = {}; @@ -1613,13 +1615,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; @@ -1637,7 +1660,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) { @@ -1658,9 +1683,9 @@ Expecting one of '${allowedValues.join("', '")}'`); value = myParseArg(declaredArg, value, declaredArg.defaultValue); } } - actionArgs[index] = value; + processedArgs[index] = value; }); - return actionArgs; + this.processedArgs = processedArgs; } /** @@ -1752,37 +1777,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 @@ -1795,14 +1805,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() } }; From c282786b80bfbf8edce8e6caec378b1b71f5b51d Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 22 May 2021 13:26:10 +1200 Subject: [PATCH 2/5] Add initialisation for this.args --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index d16a7fe7c..638009133 100644 --- a/index.js +++ b/index.js @@ -662,7 +662,7 @@ class Command extends EventEmitter { this._allowUnknownOption = false; this._allowExcessArguments = true; this._args = []; // array of Argument - this.args = undefined; // cli args with options removed + this.args = null; // cli args with options removed this.rawArgs = null; this.processedArgs = null; // like .args but after custom processing and collecting variadic this._scriptPath = null; From 83bdfcf986726dd96994e307b5f156cf6d99da3f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 22 May 2021 17:20:11 +1200 Subject: [PATCH 3/5] Add processedArgs to TypeScript --- typings/index.d.ts | 1 + typings/index.test-d.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/typings/index.d.ts b/typings/index.d.ts index abeab2be9..d6002a3bd 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -201,6 +201,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 a5edb6cf4..dcc3c3f3f 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); From b11d9ce2caf63dc8810a57ee3d27b13741c53e9a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 22 May 2021 17:21:19 +1200 Subject: [PATCH 4/5] Improve type comment --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 638009133..609c5b584 100644 --- a/index.js +++ b/index.js @@ -662,7 +662,7 @@ class Command extends EventEmitter { this._allowUnknownOption = false; this._allowExcessArguments = true; this._args = []; // array of Argument - this.args = null; // cli args with options removed + this.args = null; // array of strings, cli args with options removed this.rawArgs = null; this.processedArgs = null; // like .args but after custom processing and collecting variadic this._scriptPath = null; From 2a2e70867ecd723d1524ecaf8344682921f1f6b7 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 22 May 2021 17:44:30 +1200 Subject: [PATCH 5/5] Add JSDoc for types instead of just comments --- index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 609c5b584..633701541 100644 --- a/index.js +++ b/index.js @@ -37,6 +37,7 @@ class Help { } if (this.sortSubcommands) { visibleCommands.sort((a, b) => { + // @ts-ignore: overloaded return type return a.name().localeCompare(b.name()); }); } @@ -656,13 +657,17 @@ class Command extends EventEmitter { constructor(name) { super(); - this.commands = []; // array of Command - this.options = []; // array of Option + /** @type {Command[]} */ + this.commands = []; + /** @type {Option[]} */ + this.options = []; this.parent = null; this._allowUnknownOption = false; this._allowExcessArguments = true; - this._args = []; // array of Argument - this.args = null; // array of strings, cli args with options removed + /** @type {Argument[]} */ + this._args = []; + /** @type {string[]} */ + this.args = null; // cli args with options removed this.rawArgs = null; this.processedArgs = null; // like .args but after custom processing and collecting variadic this._scriptPath = null; @@ -2160,6 +2165,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