Skip to content

Commit

Permalink
Add support for dual long options when no short option (#2312)
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowspawn authored Jan 13, 2025
1 parent bb733f4 commit 8263b7f
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

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

Expand Down
10 changes: 9 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,15 @@ const program = new Command();

## Options

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 ('|').
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
single characters, you may also have two long options. Examples:

```js
program
.option('-p, --port <number>', 'server port number')
.option('--trace', 'add extra debugging output')
.option('--ws, --workspace <name>', 'use a custom workspace')
```

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

Expand Down
17 changes: 10 additions & 7 deletions docs/deprecated.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ They are currently still available for backwards compatibility, but should not b
- [.command('\*')](#command)
- [cmd.description(cmdDescription, argDescriptions)](#cmddescriptioncmddescription-argdescriptions)
- [InvalidOptionArgumentError](#invalidoptionargumenterror)
- [Short option flag longer than a single character](#short-option-flag-longer-than-a-single-character)
- [Import from `commander/esm.mjs`](#import-from-commanderesmmjs)
- [cmd.\_args](#cmd_args)
- [.addHelpCommand(string|boolean|undefined)](#addhelpcommandstringbooleanundefined)
- [Removed](#removed)
- [Short option flag longer than a single character](#short-option-flag-longer-than-a-single-character)
- [Default import of global Command object](#default-import-of-global-command-object)

### RegExp .option() parameter
Expand Down Expand Up @@ -168,12 +168,6 @@ function myParseInt(value, dummyPrevious) {

Deprecated from Commander v8.

### Short option flag longer than a single character

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.

README updated in Commander v3. Deprecated from Commander v9.

### Import from `commander/esm.mjs`

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

## Removed

### Short option flag longer than a single character

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.

- README updated in Commander v3.
- Deprecated from Commander v9.
- Throws an exception in Commander v13. Deprecated and gone!
- Replacement added in Commander v13.1 with support for dual long options, like `--ws, --workspace`.

### Default import of global Command object

The default import was a global Command object.
Expand Down
2 changes: 1 addition & 1 deletion lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
* @example
* program
* .option('-p, --pepper', 'add pepper')
* .option('-p, --pizza-type <TYPE>', 'type of pizza') // required option-argument
* .option('--pt, --pizza-type <TYPE>', 'type of pizza') // required option-argument
* .option('-c, --cheese [CHEESE]', 'add extra cheese', 'mozzarella') // optional option-argument with default
* .option('-t, --tip <VALUE>', 'add tip to purchase cost', parseFloat) // custom parse function
*
Expand Down
49 changes: 34 additions & 15 deletions lib/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Option {
this.variadic = /\w\.\.\.[>\]]$/.test(flags); // The option can take multiple values.
this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
const optionFlags = splitOptionFlags(flags);
this.short = optionFlags.shortFlag;
this.short = optionFlags.shortFlag; // May be a short flag, undefined, or even a long flag (if option has two long flags).
this.long = optionFlags.longFlag;
this.negate = false;
if (this.long) {
Expand Down Expand Up @@ -321,25 +321,44 @@ function splitOptionFlags(flags) {
const longFlagExp = /^--[^-]/;

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

// Check for some unsupported flags that people try.
if (/^-[^-][^-]/.test(flagParts[0]))
throw new Error(
`invalid Option flags, short option is dash and single character: '${flags}'`,
);
if (shortFlag && shortFlagExp.test(flagParts[0]))
throw new Error(
`invalid Option flags, more than one short flag: '${flags}'`,
);
if (longFlag && longFlagExp.test(flagParts[0]))
// Check for unprocessed flag. Fail noisily rather than silently ignore.
if (flagParts[0].startsWith('-')) {
const unsupportedFlag = flagParts[0];
const baseError = `option creation failed due to '${unsupportedFlag}' in option flags '${flags}'`;
if (/^-[^-][^-]/.test(unsupportedFlag))
throw new Error(
`${baseError}
- a short flag is a single dash and a single character
- either use a single dash and a single character (for a short flag)
- or use a double dash for a long option (and can have two, like '--ws, --workspace')`,
);
if (shortFlagExp.test(unsupportedFlag))
throw new Error(`${baseError}
- too many short flags`);
if (longFlagExp.test(unsupportedFlag))
throw new Error(`${baseError}
- too many long flags`);

throw new Error(`${baseError}
- unrecognised flag format`);
}
if (shortFlag === undefined && longFlag === undefined)
throw new Error(
`invalid Option flags, more than one long flag: '${flags}'`,
`option creation failed due to no flags found in '${flags}'.`,
);
// Generic error if failed to find a flag or an unexpected flag left over.
if (!(shortFlag || longFlag) || flagParts[0].startsWith('-'))
throw new Error(`invalid Option flags: '${flags}'`);

return { shortFlag, longFlag };
}
Expand Down
8 changes: 5 additions & 3 deletions tests/option.bad-flags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,26 @@ test.each([
{ flags: '-a, -b' }, // too many short flags
{ flags: '-a, -b <value>' },
{ flags: '-a, -b, --long' },
{ flags: '--one, --two' }, // too many long flags
{ flags: '--one, --two [value]' },
{ flags: '--one, --two, --three' }, // too many long flags
{ flags: '-ws' }, // short flag with more than one character
{ flags: '---triple' }, // double dash not followed by a non-dash
{ flags: 'sdkjhskjh' }, // oops, no flags
{ flags: '-a,-b' }, // try all the separators
{ flags: '-a|-b' },
{ flags: '-a -b' },
])('when construct Option with flags %p then throw', ({ flags }) => {
expect(() => {
new Option(flags);
}).toThrow(/^invalid Option flags/);
}).toThrow(/^option creation failed/);
});

// Check that supported flags do not throw.
test.each([
{ flags: '-s' }, // single short
{ flags: '--long' }, // single long
{ flags: '-b, --both' }, // short and long
{ flags: '--both, -b' }, // long and short
{ flags: '--ws, --workspace' }, // two long (morally shortish and long)
{ flags: '-b,--both <comma>' },
{ flags: '-b|--both <bar>' },
{ flags: '-b --both [space]' },
Expand Down
2 changes: 1 addition & 1 deletion typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ export class Command {
* ```js
* program
* .option('-p, --pepper', 'add pepper')
* .option('-p, --pizza-type <TYPE>', 'type of pizza') // required option-argument
* .option('--pt, --pizza-type <TYPE>', 'type of pizza') // required option-argument
* .option('-c, --cheese [CHEESE]', 'add extra cheese', 'mozzarella') // optional option-argument with default
* .option('-t, --tip <VALUE>', 'add tip to purchase cost', parseFloat) // custom parse function
* ```
Expand Down

0 comments on commit 8263b7f

Please sign in to comment.