Skip to content

Commit 1691344

Browse files
authored
Combine normalize and parseoptions, and update combined option handling (#1152)
* Add tests for Utility Conventions before changing code to match * Switch from preflight including normalise to just testing parseOptions * Only refactor known options Do the work in .parseOptions and delete single-pass .normalize * Add short flag processing to READMEm, and literal -- * Improve character description * Add a note that options not positional. * Remove regression tests for bug not really fixed by this * Add back #561 into known issues * Refactor to make a little clearer and symmetrical * Use template to construct strings consistently within parseOptions
1 parent 62234f6 commit 1691344

File tree

6 files changed

+187
-105
lines changed

6 files changed

+187
-105
lines changed

Readme.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,18 @@ For larger programs which may use commander in multiple ways, including unit tes
6767

6868
## Options
6969

70-
Options are defined with the `.option()` method, also serving as documentation for the options. Each option can have a short flag (single character) and a long name, separated by a comma or space.
70+
Options are defined with the `.option()` method, also serving as documentation for the options. Each option can have a short flag (single character) and a long name, separated by a comma or space or vertical bar ('|').
7171

72-
The options can be accessed as properties on the Command object. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc. Multiple short flags may be combined as a single arg, for example `-abc` is equivalent to `-a -b -c`.
72+
The options can be accessed as properties on the Command object. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc. See also optional new behaviour to [avoid name clashes](#avoiding-option-name-clashes).
7373

74-
See also optional new behaviour to [avoid name clashes](#avoiding-option-name-clashes).
74+
Multiple short flags may optionally be combined in a single argument following the dash: boolean flags, the last flag may take a value, and the value.
75+
For example `-a -b -p 80` may be written as `-ab -p80` or even `-abp80`.
76+
77+
You can use `--` to indicate the end of the options, and any remaining arguments will be used without being interpreted.
78+
This is particularly useful for passing options through to another
79+
command, like: `do -- git --version`.
80+
81+
Options on the command line are not positional, and can be specified before or after other command arguments.
7582

7683
### Common option types, boolean and value
7784

@@ -251,7 +258,7 @@ $ custom --list x,y,z
251258
252259
### Required option
253260
254-
You may specify a required (mandatory) option using `.requiredOption`. The option must be specified on the command line, or by having a default value. The method is otherwise the same as `.option` in format, taking flags and description, and optional default value or custom processing.
261+
You may specify a required (mandatory) option using `.requiredOption`. The option must have a value after parsing, usually specified on the command line, or perhaps from a default value (say from environment). The method is otherwise the same as `.option` in format, taking flags and description, and optional default value or custom processing.
255262
256263
```js
257264
const program = require('commander');

index.js

Lines changed: 57 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -637,9 +637,8 @@ Command.prototype.parse = function(argv) {
637637
argv.push(this._helpLongFlag);
638638
}
639639

640-
// process argv
641-
const normalized = this.normalize(argv.slice(2));
642-
const parsed = this.parseOptions(normalized);
640+
// process argv, leaving off first two args which are app and scriptname.
641+
const parsed = this.parseOptions(argv.slice(2));
643642
const args = parsed.operands.concat(parsed.unknown);
644643
this.args = args.slice();
645644

@@ -822,57 +821,6 @@ Command.prototype.executeSubCommand = function(argv, args, executableFile) {
822821
this.runningCommand = proc;
823822
};
824823

825-
/**
826-
* Normalize `args`, splitting joined short flags. For example
827-
* the arg "-abc" is equivalent to "-a -b -c".
828-
* This also normalizes equal sign and splits "--abc=def" into "--abc def".
829-
*
830-
* @param {Array} args
831-
* @return {Array}
832-
* @api private
833-
*/
834-
835-
Command.prototype.normalize = function(args) {
836-
var ret = [],
837-
arg,
838-
lastOpt,
839-
index,
840-
short,
841-
opt;
842-
843-
for (var i = 0, len = args.length; i < len; ++i) {
844-
arg = args[i];
845-
if (i > 0) {
846-
lastOpt = this.optionFor(args[i - 1]);
847-
}
848-
849-
if (arg === '--') {
850-
// Honor option terminator
851-
ret = ret.concat(args.slice(i));
852-
break;
853-
} else if (lastOpt && lastOpt.required) {
854-
ret.push(arg);
855-
} else if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') {
856-
short = arg.slice(0, 2);
857-
opt = this.optionFor(short);
858-
if (opt && (opt.required || opt.optional)) {
859-
ret.push(short);
860-
ret.push(arg.slice(2));
861-
} else {
862-
arg.slice(1).split('').forEach(function(c) {
863-
ret.push('-' + c);
864-
});
865-
}
866-
} else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
867-
ret.push(arg.slice(0, index), arg.slice(index + 1));
868-
} else {
869-
ret.push(arg);
870-
}
871-
}
872-
873-
return ret;
874-
};
875-
876824
/**
877825
* Parse command `args`.
878826
*
@@ -960,56 +908,78 @@ Command.prototype._checkForMissingMandatoryOptions = function() {
960908
Command.prototype.parseOptions = function(argv) {
961909
const operands = []; // operands, not options or values
962910
const unknown = []; // first unknown option and remaining unknown args
963-
let literal = false;
964911
let dest = operands;
912+
const args = argv.slice();
913+
914+
function maybeOption(arg) {
915+
return arg.length > 1 && arg[0] === '-';
916+
}
965917

966918
// parse options
967-
for (var i = 0; i < argv.length; ++i) {
968-
const arg = argv[i];
919+
while (args.length) {
920+
const arg = args.shift();
969921

970-
// literal args after --
971-
if (literal) {
972-
dest.push(arg);
973-
continue;
922+
// literal
923+
if (arg === '--') {
924+
if (dest === unknown) dest.push(arg);
925+
dest.push(...args);
926+
break;
974927
}
975928

976-
if (arg === '--') {
977-
literal = true;
978-
if (dest === unknown) dest.push('--');
979-
continue;
929+
if (maybeOption(arg)) {
930+
const option = this.optionFor(arg);
931+
// recognised option, call listener to assign value with possible custom processing
932+
if (option) {
933+
if (option.required) {
934+
const value = args.shift();
935+
if (value === undefined) this.optionMissingArgument(option);
936+
this.emit(`option:${option.name()}`, value);
937+
} else if (option.optional) {
938+
let value = null;
939+
// historical behaviour is optional value is following arg unless an option
940+
if (args.length > 0 && !maybeOption(args[0])) {
941+
value = args.shift();
942+
}
943+
this.emit(`option:${option.name()}`, value);
944+
} else { // boolean flag
945+
this.emit(`option:${option.name()}`);
946+
}
947+
continue;
948+
}
980949
}
981950

982-
// find matching Option
983-
const option = this.optionFor(arg);
984-
985-
// recognised option, call listener to assign value with possible custom processing
986-
if (option) {
987-
if (option.required) {
988-
const value = argv[++i];
989-
if (value === undefined) this.optionMissingArgument(option);
990-
this.emit('option:' + option.name(), value);
991-
} else if (option.optional) {
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;
951+
// Look for combo options following single dash, eat first one if known.
952+
if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') {
953+
const option = this.optionFor(`-${arg[1]}`);
954+
if (option) {
955+
if (option.required || option.optional) {
956+
// option with value following in same argument
957+
this.emit(`option:${option.name()}`, arg.slice(2));
996958
} else {
997-
++i;
959+
// boolean option, emit and put back remainder of arg for further processing
960+
this.emit(`option:${option.name()}`);
961+
args.unshift(`-${arg.slice(2)}`);
998962
}
999-
this.emit('option:' + option.name(), value);
1000-
// flag
1001-
} else {
1002-
this.emit('option:' + option.name());
963+
continue;
964+
}
965+
}
966+
967+
// Look for known long flag with value, like --foo=bar
968+
if (/^--[^=]+=/.test(arg)) {
969+
const index = arg.indexOf('=');
970+
const option = this.optionFor(arg.slice(0, index));
971+
if (option && (option.required || option.optional)) {
972+
this.emit(`option:${option.name()}`, arg.slice(index + 1));
973+
continue;
1003974
}
1004-
continue;
1005975
}
1006976

1007-
// looks like an option, unknowns from here
977+
// looks like an option but unknown, unknowns from here
1008978
if (arg.length > 1 && arg[0] === '-') {
1009979
dest = unknown;
1010980
}
1011981

1012-
// arg
982+
// add arg
1013983
dest.push(arg);
1014984
}
1015985

tests/args.literal.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
const commander = require('../');
22

3-
// Guideline 10:
4-
// The first -- argument that is not an option-argument should be accepted as a delimiter indicating the end of options. Any following arguments should be treated as operands, even if they begin with the '-' character.
3+
// Utility Conventions: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
54
//
6-
// http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02
5+
// 12.2 Utility Syntax Guidelines, Guideline 10:
6+
// The first -- argument that is not an option-argument should be accepted as a delimiter indicating the end of options. Any following arguments should be treated as operands, even if they begin with the '-' character.
77

88
test('when arguments includes -- then stop processing options', () => {
99
const program = new commander.Command();

tests/command.parseOptions.test.js

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@ const path = require('path');
77

88
// Tests created from reported bugs
99
describe('regression tests', () => {
10-
// https://github.com/tj/commander.js/issues/1039
11-
// https://github.com/tj/commander.js/issues/561
12-
test('when unknown option and multiple arguments then unknown option detected', () => {
13-
const program = new commander.Command();
14-
program
15-
.exitOverride();
16-
expect(() => {
17-
program.parse(['node', 'text', '--bug', '0', '1', '2', '3']);
18-
}).toThrow();
19-
});
20-
2110
// https://github.com/tj/commander.js/issues/1032
2211
function createProgram1032() {
2312
const program = new commander.Command();
@@ -196,3 +185,106 @@ describe('parse and program.args', () => {
196185
expect(program.args).toEqual(['sub', '--sub-flag', 'arg']);
197186
});
198187
});
188+
189+
// Utility Conventions: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_01
190+
//
191+
// 12.1 Utility Argument Syntax
192+
// 2. Option-arguments are shown separated from their options by <blank> characters, except when the option-argument is
193+
// enclosed in the '[' and ']' notation to indicate that it is optional. This reflects the situation in which an
194+
// optional option-argument (if present) is included within the same argument string as the option; for a mandatory option-argument,
195+
// it is the next argument. The Utility Syntax Guidelines in Utility Syntax Guidelines require that the option be a separate argument
196+
// from its option-argument and that option-arguments not be optional, but there are some exceptions in POSIX.1-2017 to ensure
197+
// continued operation of historical applications:
198+
//
199+
// a. If the SYNOPSIS of a standard utility shows an option with a mandatory option-argument (as with [ -c option_argument] in the example),
200+
// a conforming application shall use separate arguments for that option and its option-argument. However, a conforming implementation shall
201+
// also permit applications to specify the option and option-argument in the same argument string without intervening <blank> characters.
202+
//
203+
// b. If the SYNOPSIS shows an optional option-argument (as with [ -f[ option_argument]] in the example), a conforming application
204+
// shall place any option-argument for that option directly adjacent to the option in the same argument string, without intervening
205+
// <blank> characters. If the utility receives an argument containing only the option, it shall behave as specified in its description
206+
// for an omitted option-argument; it shall not treat the next argument (if any) as the option-argument for that option.
207+
//
208+
// 12.2 Utility Syntax Guidelines, Guideline 5:
209+
// One or more options without option-arguments, followed by at most one option that takes an option-argument, should be accepted when
210+
// grouped behind one '-' delimiter.
211+
212+
// Commander conformance:
213+
// - allows separate argument for required option-argument
214+
// - allows value in same argument for short flag with required option-argument
215+
// - non-conforming: allows separate argument for optional option-argument if does not start with '-'
216+
// - allows value in same argument for short flag with optional option-argument
217+
// - allows short flags as per 12.2
218+
//
219+
// The focus in this file is testing the behaviours with known vs unknown options.
220+
// See options.values.test.js for more general testing of known options.
221+
222+
describe('Utility Conventions', () => {
223+
function createProgram() {
224+
const program = new commander.Command();
225+
program
226+
.option('-a,--aaa')
227+
.option('-b,--bbb')
228+
.option('-c,--ccc <value>')
229+
.option('-d,--ddd [value]');
230+
program
231+
.action(() => { });
232+
return program;
233+
};
234+
235+
test('when program has combo known boolean short flags then arg removed', () => {
236+
const program = createProgram();
237+
const result = program.parseOptions(['-ab']);
238+
expect(result).toEqual({ operands: [], unknown: [] });
239+
expect(program.opts()).toEqual({ aaa: true, bbb: true });
240+
});
241+
242+
test('when program has combo unknown short flags then arg preserved', () => {
243+
const program = createProgram();
244+
const result = program.parseOptions(['-pq']);
245+
expect(result).toEqual({ operands: [], unknown: ['-pq'] });
246+
expect(program.opts()).toEqual({ });
247+
});
248+
249+
test('when program has combo known short option and required value then arg removed', () => {
250+
const program = createProgram();
251+
const result = program.parseOptions(['-cvalue']);
252+
expect(result).toEqual({ operands: [], unknown: [] });
253+
expect(program.opts()).toEqual({ ccc: 'value' });
254+
});
255+
256+
test('when program has combo known short option and optional value then arg removed', () => {
257+
const program = createProgram();
258+
const result = program.parseOptions(['-dvalue']);
259+
expect(result).toEqual({ operands: [], unknown: [] });
260+
expect(program.opts()).toEqual({ ddd: 'value' });
261+
});
262+
263+
test('when program has known combo short boolean flags and required value then arg removed', () => {
264+
const program = createProgram();
265+
const result = program.parseOptions(['-abcvalue']);
266+
expect(result).toEqual({ operands: [], unknown: [] });
267+
expect(program.opts()).toEqual({ aaa: true, bbb: true, ccc: 'value' });
268+
});
269+
270+
test('when program has known combo short boolean flags and optional value then arg removed', () => {
271+
const program = createProgram();
272+
const result = program.parseOptions(['-abdvalue']);
273+
expect(result).toEqual({ operands: [], unknown: [] });
274+
expect(program.opts()).toEqual({ aaa: true, bbb: true, ddd: 'value' });
275+
});
276+
277+
test('when program has known long flag=value then arg removed', () => {
278+
const program = createProgram();
279+
const result = program.parseOptions(['--ccc=value']);
280+
expect(result).toEqual({ operands: [], unknown: [] });
281+
expect(program.opts()).toEqual({ ccc: 'value' });
282+
});
283+
284+
test('when program has unknown long flag=value then arg preserved', () => {
285+
const program = createProgram();
286+
const result = program.parseOptions(['--rrr=value']);
287+
expect(result).toEqual({ operands: [], unknown: ['--rrr=value'] });
288+
expect(program.opts()).toEqual({ });
289+
});
290+
});

tests/openIssues.test.js.skip

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
const commander = require('../');
22

33
describe('open issues', () => {
4+
// https://github.com/tj/commander.js/issues/561
5+
test('#561: when specify argument and unknown option with no action handler then error', () => {
6+
const program = new commander.Command();
7+
program
8+
.exitOverride()
9+
.option('-x, --x-flag', 'A flag')
10+
.arguments('<file>');
11+
12+
expect(() => {
13+
program.parse(['node', 'test', '1', '--NONSENSE', '2', '3']);
14+
}).toThrow();
15+
});
416
});

tests/options.values.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const commander = require('../');
22

33
// Test the ways values can be specified for options.
4+
// See also references on "Utility Conventions" in command.parseOptions.test.js
45

56
// options with required values can eat values starting with a dash, including just dash sometimes used as alias for stdin
67
//

0 commit comments

Comments
 (0)