Skip to content

Commit 4663597

Browse files
authored
Support argument processing without action handler (#1529)
* Process command-arguments without needing action handler * Add some test for new behaviours without action handler * Modify README
1 parent 082717f commit 4663597

File tree

6 files changed

+94
-31
lines changed

6 files changed

+94
-31
lines changed

Readme.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -489,10 +489,12 @@ program
489489
490490
#### Custom argument processing
491491
492-
You may specify a function to do custom processing of command-arguments before they are passed to the action handler.
492+
You may specify a function to do custom processing of command-arguments (like for option-arguments).
493493
The callback function receives two parameters, the user specified command-argument and the previous value for the argument.
494494
It returns the new value for the argument.
495495
496+
The processed argument values are passed to the action handler, and saved as `.processedArgs`.
497+
496498
You can optionally specify the default/starting value for the argument after the function parameter.
497499
498500
Example file: [arguments-custom-processing.js](./examples/arguments-custom-processing.js)

lib/command.js

+42-27
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,19 @@ class Command extends EventEmitter {
1919

2020
constructor(name) {
2121
super();
22+
/** @type {Command[]} */
2223
this.commands = [];
24+
/** @type {Option[]} */
2325
this.options = [];
2426
this.parent = null;
2527
this._allowUnknownOption = false;
2628
this._allowExcessArguments = true;
29+
/** @type {Argument[]} */
2730
this._args = [];
28-
this.rawArgs = null;
31+
/** @type {string[]} */
32+
this.args = []; // cli args with options removed
33+
this.rawArgs = [];
34+
this.processedArgs = []; // like .args but after custom processing and collecting variadic
2935
this._scriptPath = null;
3036
this._name = name || '';
3137
this._optionValues = {};
@@ -991,13 +997,34 @@ Expecting one of '${allowedValues.join("', '")}'`);
991997
};
992998

993999
/**
994-
* Package arguments (this.args) for passing to action handler based
995-
* on declared arguments (this._args).
1000+
* Check this.args against expected this._args.
9961001
*
9971002
* @api private
9981003
*/
9991004

1000-
_getActionArguments() {
1005+
_checkNumberOfArguments() {
1006+
// too few
1007+
this._args.forEach((arg, i) => {
1008+
if (arg.required && this.args[i] == null) {
1009+
this.missingArgument(arg.name());
1010+
}
1011+
});
1012+
// too many
1013+
if (this._args.length > 0 && this._args[this._args.length - 1].variadic) {
1014+
return;
1015+
}
1016+
if (this.args.length > this._args.length) {
1017+
this._excessArguments(this.args);
1018+
}
1019+
};
1020+
1021+
/**
1022+
* Process this.args using this._args and save as this.processedArgs!
1023+
*
1024+
* @api private
1025+
*/
1026+
1027+
_processArguments() {
10011028
const myParseArg = (argument, value, previous) => {
10021029
// Extra processing for nice error message on parsing failure.
10031030
let parsedValue = value;
@@ -1015,7 +1042,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
10151042
return parsedValue;
10161043
};
10171044

1018-
const actionArgs = [];
1045+
this._checkNumberOfArguments();
1046+
1047+
const processedArgs = [];
10191048
this._args.forEach((declaredArg, index) => {
10201049
let value = declaredArg.defaultValue;
10211050
if (declaredArg.variadic) {
@@ -1036,9 +1065,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
10361065
value = myParseArg(declaredArg, value, declaredArg.defaultValue);
10371066
}
10381067
}
1039-
actionArgs[index] = value;
1068+
processedArgs[index] = value;
10401069
});
1041-
return actionArgs;
1070+
this.processedArgs = processedArgs;
10421071
}
10431072

10441073
/**
@@ -1130,37 +1159,22 @@ Expecting one of '${allowedValues.join("', '")}'`);
11301159
this.unknownOption(parsed.unknown[0]);
11311160
}
11321161
};
1133-
const checkNumberOfArguments = () => {
1134-
// too few
1135-
this._args.forEach((arg, i) => {
1136-
if (arg.required && this.args[i] == null) {
1137-
this.missingArgument(arg.name());
1138-
}
1139-
});
1140-
// too many
1141-
if (this._args.length > 0 && this._args[this._args.length - 1].variadic) {
1142-
return;
1143-
}
1144-
if (this.args.length > this._args.length) {
1145-
this._excessArguments(this.args);
1146-
}
1147-
};
11481162

11491163
const commandEvent = `command:${this.name()}`;
11501164
if (this._actionHandler) {
11511165
checkForUnknownOptions();
1152-
checkNumberOfArguments();
1166+
this._processArguments();
11531167

11541168
let actionResult;
11551169
actionResult = this._chainOrCallHooks(actionResult, 'preAction');
1156-
actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this._getActionArguments()));
1170+
actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs));
11571171
if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy
11581172
actionResult = this._chainOrCallHooks(actionResult, 'postAction');
11591173
return actionResult;
11601174
}
11611175
if (this.parent && this.parent.listenerCount(commandEvent)) {
11621176
checkForUnknownOptions();
1163-
checkNumberOfArguments();
1177+
this._processArguments();
11641178
this.parent.emit(commandEvent, operands, unknown); // legacy
11651179
} else if (operands.length) {
11661180
if (this._findCommand('*')) { // legacy default command
@@ -1173,14 +1187,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
11731187
this.unknownCommand();
11741188
} else {
11751189
checkForUnknownOptions();
1176-
checkNumberOfArguments();
1190+
this._processArguments();
11771191
}
11781192
} else if (this.commands.length) {
11791193
// This command has subcommands and nothing hooked up at this level, so display help (and exit).
11801194
this.help({ error: true });
11811195
} else {
11821196
checkForUnknownOptions();
1183-
checkNumberOfArguments();
1197+
this._processArguments();
11841198
// fall through for caller to handle after calling .parse()
11851199
}
11861200
};
@@ -1528,6 +1542,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
15281542
alias(alias) {
15291543
if (alias === undefined) return this._aliases[0]; // just return first, for backwards compatibility
15301544

1545+
/** @type {Command} */
15311546
let command = this;
15321547
if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) {
15331548
// assume adding alias for last added executable subcommand, rather than this

lib/help.js

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Help {
3838
}
3939
if (this.sortSubcommands) {
4040
visibleCommands.sort((a, b) => {
41+
// @ts-ignore: overloaded return type
4142
return a.name().localeCompare(b.name());
4243
});
4344
}

tests/argument.custom-processing.test.js

+45-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const commander = require('../');
22

33
// Testing default value and custom processing behaviours.
4+
// Some double assertions in tests to check action argument and .processedArg
45

56
test('when argument not specified then callback not called', () => {
67
const mockCoercion = jest.fn();
@@ -34,7 +35,7 @@ test('when custom with starting value and argument not specified then callback n
3435
expect(mockCoercion).not.toHaveBeenCalled();
3536
});
3637

37-
test('when custom with starting value and argument not specified then action argument is starting value', () => {
38+
test('when custom with starting value and argument not specified with action handler then action argument is starting value', () => {
3839
const startingValue = 1;
3940
let actionValue;
4041
const program = new commander.Command();
@@ -45,9 +46,19 @@ test('when custom with starting value and argument not specified then action arg
4546
});
4647
program.parse([], { from: 'user' });
4748
expect(actionValue).toEqual(startingValue);
49+
expect(program.processedArgs).toEqual([startingValue]);
4850
});
4951

50-
test('when default value is defined (without custom processing) and argument not specified then action argument is default value', () => {
52+
test('when custom with starting value and argument not specified without action handler then .processedArgs has starting value', () => {
53+
const startingValue = 1;
54+
const program = new commander.Command();
55+
program
56+
.argument('[n]', 'number', parseFloat, startingValue);
57+
program.parse([], { from: 'user' });
58+
expect(program.processedArgs).toEqual([startingValue]);
59+
});
60+
61+
test('when default value is defined (without custom processing) and argument not specified with action handler then action argument is default value', () => {
5162
const defaultValue = 1;
5263
let actionValue;
5364
const program = new commander.Command();
@@ -58,6 +69,16 @@ test('when default value is defined (without custom processing) and argument not
5869
});
5970
program.parse([], { from: 'user' });
6071
expect(actionValue).toEqual(defaultValue);
72+
expect(program.processedArgs).toEqual([defaultValue]);
73+
});
74+
75+
test('when default value is defined (without custom processing) and argument not specified without action handler then .processedArgs is default value', () => {
76+
const defaultValue = 1;
77+
const program = new commander.Command();
78+
program
79+
.argument('[n]', 'number', defaultValue);
80+
program.parse([], { from: 'user' });
81+
expect(program.processedArgs).toEqual([defaultValue]);
6182
});
6283

6384
test('when argument specified then callback called with value', () => {
@@ -71,7 +92,7 @@ test('when argument specified then callback called with value', () => {
7192
expect(mockCoercion).toHaveBeenCalledWith(value, undefined);
7293
});
7394

74-
test('when argument specified then action value is as returned from callback', () => {
95+
test('when argument specified with action handler then action value is as returned from callback', () => {
7596
const callbackResult = 2;
7697
let actionValue;
7798
const program = new commander.Command();
@@ -84,6 +105,18 @@ test('when argument specified then action value is as returned from callback', (
84105
});
85106
program.parse(['node', 'test', 'alpha']);
86107
expect(actionValue).toEqual(callbackResult);
108+
expect(program.processedArgs).toEqual([callbackResult]);
109+
});
110+
111+
test('when argument specified without action handler then .processedArgs is as returned from callback', () => {
112+
const callbackResult = 2;
113+
const program = new commander.Command();
114+
program
115+
.argument('[n]', 'number', () => {
116+
return callbackResult;
117+
});
118+
program.parse(['node', 'test', 'alpha']);
119+
expect(program.processedArgs).toEqual([callbackResult]);
87120
});
88121

89122
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
124157
expect(mockCoercion).toHaveBeenNthCalledWith(2, '2', 'callback');
125158
});
126159

160+
test('when variadic argument without action handler then .processedArg has array', () => {
161+
const program = new commander.Command();
162+
program
163+
.argument('<n...>', 'number');
164+
program.parse(['1', '2'], { from: 'user' });
165+
expect(program.processedArgs).toEqual([['1', '2']]);
166+
});
167+
127168
test('when parseFloat "1e2" then action argument is 100', () => {
128169
let actionValue;
129170
const program = new commander.Command();
@@ -134,6 +175,7 @@ test('when parseFloat "1e2" then action argument is 100', () => {
134175
});
135176
program.parse(['1e2'], { from: 'user' });
136177
expect(actionValue).toEqual(100);
178+
expect(program.processedArgs).toEqual([actionValue]);
137179
});
138180

139181
test('when defined default value for required argument then throw', () => {

typings/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export interface OptionValues {
217217

218218
export class Command {
219219
args: string[];
220+
processedArgs: any[];
220221
commands: Command[];
221222
parent: Command | null;
222223

typings/index.test-d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ expectType<commander.Argument>(commander.createArgument('<foo>'));
2525

2626
// Command properties
2727
expectType<string[]>(program.args);
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
expectType<any[]>(program.processedArgs);
2830
expectType<commander.Command[]>(program.commands);
2931
expectType<commander.Command | null>(program.parent);
3032

0 commit comments

Comments
 (0)