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

Support argument processing without action handler #1526

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion Readme.md
Expand Up @@ -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.

Expand Down
68 changes: 42 additions & 26 deletions index.js
Expand Up @@ -37,6 +37,7 @@ class Help {
}
if (this.sortSubcommands) {
visibleCommands.sort((a, b) => {
// @ts-ignore: overloaded return type
return a.name().localeCompare(b.name());
});
}
Expand Down Expand Up @@ -656,13 +657,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 = [];
/** @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;
this._name = name || '';
this._optionValues = {};
Expand Down Expand Up @@ -1613,13 +1620,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;
Expand All @@ -1637,7 +1665,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) {
Expand All @@ -1658,9 +1688,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
value = myParseArg(declaredArg, value, declaredArg.defaultValue);
}
}
actionArgs[index] = value;
processedArgs[index] = value;
});
return actionArgs;
this.processedArgs = processedArgs;
}

/**
Expand Down Expand Up @@ -1752,37 +1782,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
Expand All @@ -1795,14 +1810,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()
}
};
Expand Down Expand Up @@ -2150,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
Expand Down
1 change: 1 addition & 0 deletions typings/index.d.ts
Expand Up @@ -201,6 +201,7 @@ export interface OptionValues {

export class Command {
args: string[];
processedArgs: any[];
commands: Command[];
parent: Command | null;

Expand Down
2 changes: 2 additions & 0 deletions typings/index.test-d.ts
Expand Up @@ -25,6 +25,8 @@ expectType<commander.Argument>(commander.createArgument('<foo>'));

// Command properties
expectType<string[]>(program.args);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expectType<any[]>(program.processedArgs);
expectType<commander.Command[]>(program.commands);
expectType<commander.Command | null>(program.parent);

Expand Down