Skip to content

Commit 8263b7f

Browse files
authored
Add support for dual long options when no short option (#2312)
1 parent bb733f4 commit 8263b7f

File tree

7 files changed

+61
-28
lines changed

7 files changed

+61
-28
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2323

2424
- *Breaking*: excess command-arguments cause an error by default, see migration tips ([#2223])
2525
- *Breaking*: throw during Option construction for unsupported option flags, like multiple characters after single `-` ([#2270])
26+
- note: support for dual long option flags added in Commander 13.1
2627
- *Breaking*: throw on multiple calls to `.parse()` if `storeOptionsAsProperties: true` ([#2299])
2728
- TypeScript: include implicit `this` in parameters for action handler callback ([#2197])
2829

Readme.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,15 @@ const program = new Command();
175175

176176
## Options
177177

178-
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 ('|').
178+
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 ('|'). To allow a wider range of short-ish flags than just
179+
single characters, you may also have two long options. Examples:
180+
181+
```js
182+
program
183+
.option('-p, --port <number>', 'server port number')
184+
.option('--trace', 'add extra debugging output')
185+
.option('--ws, --workspace <name>', 'use a custom workspace')
186+
```
179187

180188
The parsed options can be accessed by calling `.opts()` on a `Command` object, and are passed to the action handler.
181189

docs/deprecated.md

+10-7
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ They are currently still available for backwards compatibility, but should not b
1212
- [.command('\*')](#command)
1313
- [cmd.description(cmdDescription, argDescriptions)](#cmddescriptioncmddescription-argdescriptions)
1414
- [InvalidOptionArgumentError](#invalidoptionargumenterror)
15-
- [Short option flag longer than a single character](#short-option-flag-longer-than-a-single-character)
1615
- [Import from `commander/esm.mjs`](#import-from-commanderesmmjs)
1716
- [cmd.\_args](#cmd_args)
1817
- [.addHelpCommand(string|boolean|undefined)](#addhelpcommandstringbooleanundefined)
1918
- [Removed](#removed)
19+
- [Short option flag longer than a single character](#short-option-flag-longer-than-a-single-character)
2020
- [Default import of global Command object](#default-import-of-global-command-object)
2121

2222
### RegExp .option() parameter
@@ -168,12 +168,6 @@ function myParseInt(value, dummyPrevious) {
168168

169169
Deprecated from Commander v8.
170170

171-
### Short option flag longer than a single character
172-
173-
Short option flags like `-ws` were never supported, but the old README did not make this clear. The README now states that short options are a single character.
174-
175-
README updated in Commander v3. Deprecated from Commander v9.
176-
177171
### Import from `commander/esm.mjs`
178172

179173
The first support for named imports required an explicit entry file.
@@ -232,6 +226,15 @@ program.addHelpCommand(new Command('assist').argument('[command]').description('
232226

233227
## Removed
234228

229+
### Short option flag longer than a single character
230+
231+
Short option flags like `-ws` were never supported, but the old README did not make this clear. The README now states that short options are a single character.
232+
233+
- README updated in Commander v3.
234+
- Deprecated from Commander v9.
235+
- Throws an exception in Commander v13. Deprecated and gone!
236+
- Replacement added in Commander v13.1 with support for dual long options, like `--ws, --workspace`.
237+
235238
### Default import of global Command object
236239

237240
The default import was a global Command object.

lib/command.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
756756
* @example
757757
* program
758758
* .option('-p, --pepper', 'add pepper')
759-
* .option('-p, --pizza-type <TYPE>', 'type of pizza') // required option-argument
759+
* .option('--pt, --pizza-type <TYPE>', 'type of pizza') // required option-argument
760760
* .option('-c, --cheese [CHEESE]', 'add extra cheese', 'mozzarella') // optional option-argument with default
761761
* .option('-t, --tip <VALUE>', 'add tip to purchase cost', parseFloat) // custom parse function
762762
*

lib/option.js

+34-15
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Option {
1818
this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
1919
this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
2020
const optionFlags = splitOptionFlags(flags);
21-
this.short = optionFlags.shortFlag;
21+
this.short = optionFlags.shortFlag; // May be a short flag, undefined, or even a long flag (if option has two long flags).
2222
this.long = optionFlags.longFlag;
2323
this.negate = false;
2424
if (this.long) {
@@ -321,25 +321,44 @@ function splitOptionFlags(flags) {
321321
const longFlagExp = /^--[^-]/;
322322

323323
const flagParts = flags.split(/[ |,]+/).concat('guard');
324+
// Normal is short and/or long.
324325
if (shortFlagExp.test(flagParts[0])) shortFlag = flagParts.shift();
325326
if (longFlagExp.test(flagParts[0])) longFlag = flagParts.shift();
327+
// Long then short. Rarely used but fine.
328+
if (!shortFlag && shortFlagExp.test(flagParts[0]))
329+
shortFlag = flagParts.shift();
330+
// Allow two long flags, like '--ws, --workspace'
331+
// This is the supported way to have a shortish option flag.
332+
if (!shortFlag && longFlagExp.test(flagParts[0])) {
333+
shortFlag = longFlag;
334+
longFlag = flagParts.shift();
335+
}
326336

327-
// Check for some unsupported flags that people try.
328-
if (/^-[^-][^-]/.test(flagParts[0]))
329-
throw new Error(
330-
`invalid Option flags, short option is dash and single character: '${flags}'`,
331-
);
332-
if (shortFlag && shortFlagExp.test(flagParts[0]))
333-
throw new Error(
334-
`invalid Option flags, more than one short flag: '${flags}'`,
335-
);
336-
if (longFlag && longFlagExp.test(flagParts[0]))
337+
// Check for unprocessed flag. Fail noisily rather than silently ignore.
338+
if (flagParts[0].startsWith('-')) {
339+
const unsupportedFlag = flagParts[0];
340+
const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags}'`;
341+
if (/^-[^-][^-]/.test(unsupportedFlag))
342+
throw new Error(
343+
`${baseError}
344+
- a short flag is a single dash and a single character
345+
- either use a single dash and a single character (for a short flag)
346+
- or use a double dash for a long option (and can have two, like '--ws, --workspace')`,
347+
);
348+
if (shortFlagExp.test(unsupportedFlag))
349+
throw new Error(`${baseError}
350+
- too many short flags`);
351+
if (longFlagExp.test(unsupportedFlag))
352+
throw new Error(`${baseError}
353+
- too many long flags`);
354+
355+
throw new Error(`${baseError}
356+
- unrecognised flag format`);
357+
}
358+
if (shortFlag === undefined && longFlag === undefined)
337359
throw new Error(
338-
`invalid Option flags, more than one long flag: '${flags}'`,
360+
`option creation failed due to no flags found in '${flags}'.`,
339361
);
340-
// Generic error if failed to find a flag or an unexpected flag left over.
341-
if (!(shortFlag || longFlag) || flagParts[0].startsWith('-'))
342-
throw new Error(`invalid Option flags: '${flags}'`);
343362

344363
return { shortFlag, longFlag };
345364
}

tests/option.bad-flags.test.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@ test.each([
55
{ flags: '-a, -b' }, // too many short flags
66
{ flags: '-a, -b <value>' },
77
{ flags: '-a, -b, --long' },
8-
{ flags: '--one, --two' }, // too many long flags
9-
{ flags: '--one, --two [value]' },
8+
{ flags: '--one, --two, --three' }, // too many long flags
109
{ flags: '-ws' }, // short flag with more than one character
10+
{ flags: '---triple' }, // double dash not followed by a non-dash
1111
{ flags: 'sdkjhskjh' }, // oops, no flags
1212
{ flags: '-a,-b' }, // try all the separators
1313
{ flags: '-a|-b' },
1414
{ flags: '-a -b' },
1515
])('when construct Option with flags %p then throw', ({ flags }) => {
1616
expect(() => {
1717
new Option(flags);
18-
}).toThrow(/^invalid Option flags/);
18+
}).toThrow(/^option creation failed/);
1919
});
2020

2121
// Check that supported flags do not throw.
2222
test.each([
2323
{ flags: '-s' }, // single short
2424
{ flags: '--long' }, // single long
2525
{ flags: '-b, --both' }, // short and long
26+
{ flags: '--both, -b' }, // long and short
27+
{ flags: '--ws, --workspace' }, // two long (morally shortish and long)
2628
{ flags: '-b,--both <comma>' },
2729
{ flags: '-b|--both <bar>' },
2830
{ flags: '-b --both [space]' },

typings/index.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ export class Command {
617617
* ```js
618618
* program
619619
* .option('-p, --pepper', 'add pepper')
620-
* .option('-p, --pizza-type <TYPE>', 'type of pizza') // required option-argument
620+
* .option('--pt, --pizza-type <TYPE>', 'type of pizza') // required option-argument
621621
* .option('-c, --cheese [CHEESE]', 'add extra cheese', 'mozzarella') // optional option-argument with default
622622
* .option('-t, --tip <VALUE>', 'add tip to purchase cost', parseFloat) // custom parse function
623623
* ```

0 commit comments

Comments
 (0)