Skip to content

Commit

Permalink
Add support for getting merged options including globals (#1671)
Browse files Browse the repository at this point in the history
* Proof of concept

* Use separate method for optsWithGlobals

* Add documentation

* Add tests

* Simplify description

* Add example

* Remove unused param
  • Loading branch information
shadowspawn authored Jan 11, 2022
1 parent f902f6d commit 772eb53
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 4 deletions.
8 changes: 6 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,6 @@ const program = new Command();
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 ('|').

The parsed options can be accessed by calling `.opts()` on a `Command` object, and are passed to the action handler.
(You can also use `.getOptionValue()` and `.setOptionValue()` to work with a single option value,
and `.getOptionValueSource()` and `.setOptionValueWithSource()` when it matters where the option value came from.)

Multi-word options such as "--template-engine" are camel-cased, becoming `program.opts().templateEngine` etc.

Expand All @@ -186,6 +184,12 @@ You can use `--` to indicate the end of the options, and any remaining arguments

By default options on the command line are not positional, and can be specified before or after other arguments.

There are additional related routines for when `.opts()` is not enough:

- `.optsWithGlobals()` returns merged local and global option values
- `.getOptionValue()` and `.setOptionValue()` work with a single option value
- `.getOptionValueSource()` and `.setOptionValueWithSource()` include where the option value came from

### Common option types, boolean and value

The two most used option types are a boolean option, and an option which takes its value
Expand Down
24 changes: 24 additions & 0 deletions examples/optsWithGlobals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// const { Command } = require('commander'); // (normal include)
const { Command } = require('../'); // include commander in git clone of commander repo

// Show use of .optsWithGlobals(), and compare with .opts().

const program = new Command();

program
.option('-g, --global');

program
.command('sub')
.option('-l, --local')
.action((options, cmd) => {
console.log({
opts: cmd.opts(),
optsWithGlobals: cmd.optsWithGlobals()
});
});

program.parse();

// Try the following:
// node optsWithGlobals.js --global sub --local
15 changes: 14 additions & 1 deletion lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -1441,7 +1441,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
}

/**
* Return an object containing options as key-value pairs
* Return an object containing local option values as key-value pairs.
*
* @return {Object}
*/
Expand All @@ -1461,6 +1461,19 @@ Expecting one of '${allowedValues.join("', '")}'`);
return this._optionValues;
}

/**
* Return an object containing merged local and global option values as key-value pairs.
*
* @return {Object}
*/
optsWithGlobals() {
// globals overwrite locals
return getCommandAndParents(this).reduce(
(combinedOptions, cmd) => Object.assign(combinedOptions, cmd.opts()),
{}
);
}

/**
* Internal bottleneck for handling of parsing errors.
*
Expand Down
63 changes: 63 additions & 0 deletions tests/options.optsWithGlobals.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const commander = require('../');

test('when variety of options used with program then opts is same as optsWithGlobals', () => {
const program = new commander.Command();
program
.option('-b, --boolean')
.option('-r, --require-value <value)')
.option('-f, --float <value>', 'description', parseFloat)
.option('-d, --default-value <value)', 'description', 'default value')
.option('-n, --no-something');

program.parse(['-b', '-r', 'req', '-f', '1e2'], { from: 'user' });
expect(program.opts()).toEqual(program.optsWithGlobals());
});

test('when options in sub and program then optsWithGlobals includes both', () => {
const program = new commander.Command();
let mergedOptions;
program
.option('-g, --global <value>');
program
.command('sub')
.option('-l, --local <value)')
.action((options, cmd) => {
mergedOptions = cmd.optsWithGlobals();
});

program.parse(['-g', 'GGG', 'sub', '-l', 'LLL'], { from: 'user' });
expect(mergedOptions).toEqual({ global: 'GGG', local: 'LLL' });
});

test('when options in sub and subsub then optsWithGlobals includes both', () => {
const program = new commander.Command();
let mergedOptions;
program
.command('sub')
.option('-g, --global <value)')
.command('subsub')
.option('-l, --local <value)')
.action((options, cmd) => {
mergedOptions = cmd.optsWithGlobals();
});

program.parse(['sub', '-g', 'GGG', 'subsub', '-l', 'LLL'], { from: 'user' });
expect(mergedOptions).toEqual({ global: 'GGG', local: 'LLL' });
});

test('when same named option in sub and program then optsWithGlobals includes global', () => {
const program = new commander.Command();
let mergedOptions;
program
.option('-c, --common <value>')
.enablePositionalOptions();
program
.command('sub')
.option('-c, --common <value)')
.action((options, cmd) => {
mergedOptions = cmd.optsWithGlobals();
});

program.parse(['-c', 'GGG', 'sub', '-c', 'LLL'], { from: 'user' });
expect(mergedOptions).toEqual({ common: 'GGG' });
});
7 changes: 6 additions & 1 deletion typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,10 +659,15 @@ export class Command {
parseOptions(argv: string[]): ParseOptionsResult;

/**
* Return an object containing options as key-value pairs
* Return an object containing local option values as key-value pairs
*/
opts<T extends OptionValues>(): T;

/**
* Return an object containing merged local and global option values as key-value pairs.
*/
optsWithGlobals<T extends OptionValues>(): T;

/**
* Set the description.
*
Expand Down
12 changes: 12 additions & 0 deletions typings/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ expectType<string>(myCheeseOption.cheese);
// @ts-expect-error Check that options strongly typed and does not allow arbitrary properties
expectType(myCheeseOption.foo);

// optsWithGlobals
const optsWithGlobals = program.optsWithGlobals();
expectType<commander.OptionValues>(optsWithGlobals);
expectType(optsWithGlobals.foo);
expectType(optsWithGlobals.bar);

// optsWithGlobals with generics
const myCheeseOptionWithGlobals = program.optsWithGlobals<MyCheeseOption>();
expectType<string>(myCheeseOptionWithGlobals.cheese);
// @ts-expect-error Check that options strongly typed and does not allow arbitrary properties
expectType(myCheeseOptionWithGlobals.foo);

// description
expectType<commander.Command>(program.description('my description'));
expectType<string>(program.description());
Expand Down

0 comments on commit 772eb53

Please sign in to comment.