Skip to content

Commit 62234f6

Browse files
authored
parseOptions rework phase 1 (#1138)
* Remove openIssues test for #1062, fixed and being tested * Rework parseOptions handling of unknown arguments * First tests for parseOptions, enable prepared regression tests * Add tests for parseOptions * Add tests on program.args after calling parse * Minor update to README for changed parse behaviour * Tweak inline .parseOption examples
1 parent c448afb commit 62234f6

File tree

6 files changed

+273
-160
lines changed

6 files changed

+273
-160
lines changed

Readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pizza details:
109109
- cheese
110110
```
111111
112-
`program.parse(arguments)` processes the arguments, leaving any args not consumed by the options as the `program.args` array.
112+
`program.parse(arguments)` processes the arguments, leaving any args not consumed by the program options in the `program.args` array.
113113
114114
### Default option value
115115
@@ -356,7 +356,7 @@ program
356356
program.parse(process.argv);
357357
```
358358
359-
The variadic argument is passed to the action handler as an array. (And this also applies to `program.args`.)
359+
The variadic argument is passed to the action handler as an array.
360360
361361
### Action handler (sub)commands
362362
@@ -559,7 +559,7 @@ program.on('option:verbose', function () {
559559
560560
// error on unknown commands
561561
program.on('command:*', function () {
562-
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
562+
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args[0]]);
563563
process.exit(1);
564564
});
565565
```

index.js

Lines changed: 55 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -334,22 +334,19 @@ Command.prototype.action = function(fn) {
334334
outputHelpIfRequested(self, parsed.unknown);
335335
self._checkForMissingMandatoryOptions();
336336

337-
// If there are still any unknown options, then we simply
338-
// die, unless someone asked for help, in which case we give it
339-
// to them, and then we die.
337+
// If there are still any unknown options, then we simply die.
340338
if (parsed.unknown.length > 0) {
341339
self.unknownOption(parsed.unknown[0]);
342340
}
343341

344-
// Leftover arguments need to be pushed back. Fixes issue #56
345-
if (parsed.args.length) args = parsed.args.concat(args);
342+
args = args.concat(parsed.operands, parsed.unknown);
346343

347-
self._args.forEach(function(arg, i) {
348-
if (arg.required && args[i] == null) {
349-
self.missingArgument(arg.name);
350-
} else if (arg.variadic) {
344+
self._args.forEach(function(expectedArg, i) {
345+
if (expectedArg.required && args[i] == null) {
346+
self.missingArgument(expectedArg.name);
347+
} else if (expectedArg.variadic) {
351348
if (i !== self._args.length - 1) {
352-
self.variadicArgNotLast(arg.name);
349+
self.variadicArgNotLast(expectedArg.name);
353350
}
354351

355352
args[i] = args.splice(i);
@@ -641,11 +638,12 @@ Command.prototype.parse = function(argv) {
641638
}
642639

643640
// process argv
644-
var normalized = this.normalize(argv.slice(2));
645-
var parsed = this.parseOptions(normalized);
646-
var args = this.args = parsed.args;
641+
const normalized = this.normalize(argv.slice(2));
642+
const parsed = this.parseOptions(normalized);
643+
const args = parsed.operands.concat(parsed.unknown);
644+
this.args = args.slice();
647645

648-
var result = this.parseArgs(this.args, parsed.unknown);
646+
var result = this.parseArgs(parsed.operands, parsed.unknown);
649647

650648
if (args[0] === 'help' && args.length === 1) this.help();
651649

@@ -695,7 +693,7 @@ Command.prototype.parse = function(argv) {
695693
}
696694

697695
if (this._execs.has(name)) {
698-
return this.executeSubCommand(argv, args, parsed.unknown, subCommand ? subCommand._executableFile : undefined);
696+
return this.executeSubCommand(argv, args, subCommand ? subCommand._executableFile : undefined);
699697
}
700698

701699
return result;
@@ -725,9 +723,7 @@ Command.prototype.parseAsync = function(argv) {
725723
* @api private
726724
*/
727725

728-
Command.prototype.executeSubCommand = function(argv, args, unknown, executableFile) {
729-
args = args.concat(unknown);
730-
726+
Command.prototype.executeSubCommand = function(argv, args, executableFile) {
731727
if (!args.length) this.help();
732728

733729
var isExplicitJS = false; // Whether to use node to launch "executable"
@@ -889,16 +885,14 @@ Command.prototype.normalize = function(args) {
889885
* @api private
890886
*/
891887

892-
Command.prototype.parseArgs = function(args, unknown) {
893-
var name;
894-
895-
if (args.length) {
896-
name = args[0];
888+
Command.prototype.parseArgs = function(operands, unknown) {
889+
if (operands.length) {
890+
const name = operands[0];
897891
if (this.listeners('command:' + name).length) {
898-
this.emit('command:' + args.shift(), args, unknown);
892+
this.emit('command:' + operands[0], operands.slice(1), unknown);
899893
} else {
900-
this.emit('program-command', args, unknown);
901-
this.emit('command:*', args, unknown);
894+
this.emit('program-command', operands, unknown);
895+
this.emit('command:*', operands, unknown);
902896
}
903897
} else {
904898
outputHelpIfRequested(this, unknown);
@@ -926,11 +920,7 @@ Command.prototype.parseArgs = function(args, unknown) {
926920
*/
927921

928922
Command.prototype.optionFor = function(arg) {
929-
for (var i = 0, len = this.options.length; i < len; ++i) {
930-
if (this.options[i].is(arg)) {
931-
return this.options[i];
932-
}
933-
}
923+
return this.options.find(option => option.is(arg));
934924
};
935925

936926
/**
@@ -951,82 +941,79 @@ Command.prototype._checkForMissingMandatoryOptions = function() {
951941
};
952942

953943
/**
954-
* Parse options from `argv` returning `argv`
955-
* void of these options.
944+
* Parse options from `argv` removing known options,
945+
* and return argv split into operands and unknown arguments.
956946
*
957-
* @param {Array} argv
958-
* @return {{args: Array, unknown: Array}}
947+
* Examples:
948+
*
949+
* argv => operands, unknown
950+
* --known kkk op => [op], []
951+
* op --known kkk => [op], []
952+
* sub --unknown uuu op => [sub], [--unknown uuu op]
953+
* sub -- --unknown uuu op => [sub --unknown uuu op], []
954+
*
955+
* @param {String[]} argv
956+
* @return {{operands: String[], unknown: String[]}}
959957
* @api public
960958
*/
961959

962960
Command.prototype.parseOptions = function(argv) {
963-
var args = [],
964-
len = argv.length,
965-
literal,
966-
option,
967-
arg;
968-
969-
var unknownOptions = [];
961+
const operands = []; // operands, not options or values
962+
const unknown = []; // first unknown option and remaining unknown args
963+
let literal = false;
964+
let dest = operands;
970965

971966
// parse options
972-
for (var i = 0; i < len; ++i) {
973-
arg = argv[i];
967+
for (var i = 0; i < argv.length; ++i) {
968+
const arg = argv[i];
974969

975970
// literal args after --
976971
if (literal) {
977-
args.push(arg);
972+
dest.push(arg);
978973
continue;
979974
}
980975

981976
if (arg === '--') {
982977
literal = true;
978+
if (dest === unknown) dest.push('--');
983979
continue;
984980
}
985981

986982
// find matching Option
987-
option = this.optionFor(arg);
983+
const option = this.optionFor(arg);
988984

989-
// option is defined
985+
// recognised option, call listener to assign value with possible custom processing
990986
if (option) {
991-
// requires arg
992987
if (option.required) {
993-
arg = argv[++i];
994-
if (arg == null) return this.optionMissingArgument(option);
995-
this.emit('option:' + option.name(), arg);
996-
// optional arg
988+
const value = argv[++i];
989+
if (value === undefined) this.optionMissingArgument(option);
990+
this.emit('option:' + option.name(), value);
997991
} else if (option.optional) {
998-
arg = argv[i + 1];
999-
if (arg == null || (arg[0] === '-' && arg !== '-')) {
1000-
arg = null;
992+
let value = argv[i + 1];
993+
// do not use a following option as a value
994+
if (value === undefined || (value[0] === '-' && value !== '-')) {
995+
value = null;
1001996
} else {
1002997
++i;
1003998
}
1004-
this.emit('option:' + option.name(), arg);
999+
this.emit('option:' + option.name(), value);
10051000
// flag
10061001
} else {
10071002
this.emit('option:' + option.name());
10081003
}
10091004
continue;
10101005
}
10111006

1012-
// looks like an option
1007+
// looks like an option, unknowns from here
10131008
if (arg.length > 1 && arg[0] === '-') {
1014-
unknownOptions.push(arg);
1015-
1016-
// If the next argument looks like it might be
1017-
// an argument for this option, we pass it on.
1018-
// If it isn't, then it'll simply be ignored
1019-
if ((i + 1) < argv.length && (argv[i + 1][0] !== '-' || argv[i + 1] === '-')) {
1020-
unknownOptions.push(argv[++i]);
1021-
}
1022-
continue;
1009+
dest = unknown;
10231010
}
10241011

10251012
// arg
1026-
args.push(arg);
1013+
dest.push(arg);
10271014
}
10281015

1029-
return { args: args, unknown: unknownOptions };
1016+
return { operands, unknown };
10301017
};
10311018

10321019
/**

tests/command.action.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ test('when .action called then program.args only contains args', () => {
2020
.command('info <file>')
2121
.action(() => {});
2222
program.parse(['node', 'test', 'info', 'my-file']);
23-
expect(program.args).toEqual(['my-file']);
23+
expect(program.args).toEqual(['info', 'my-file']);
2424
});
2525

2626
test('when .action called with extra arguments then extras also passed to action', () => {

0 commit comments

Comments
 (0)