Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into release/9.x
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowspawn committed Aug 25, 2021
2 parents b1b7aca + 8571a75 commit dd28741
Show file tree
Hide file tree
Showing 14 changed files with 484 additions and 34 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Added

- `.copyInheritedSettings()` ([#1557])
- update Chinese translation updates for Commander v8 ([#1570])
- update Chinese translations of documentation for Commander v8 ([#1570])
- `Argument` methods for `.argRequired()` and `.argOptional()` ([#1567])

## [8.0.0] (2021-06-25)
Expand Down
9 changes: 7 additions & 2 deletions Readme.md
Expand Up @@ -306,13 +306,14 @@ program.version('0.0.1', '-v, --vers', 'output the current version');
You can add most options using the `.option()` method, but there are some additional features available
by constructing an `Option` explicitly for less common cases.
Example file: [options-extra.js](./examples/options-extra.js)
Example files: [options-extra.js](./examples/options-extra.js), [options-env.js](./examples/options-env.js)
```js
program
.addOption(new Option('-s, --secret').hideHelp())
.addOption(new Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
.addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']));
.addOption(new Option('-d, --drink <size>', 'drink size').choices(['small', 'medium', 'large']))
.addOption(new Option('-p, --port <number>', 'port number').env('PORT'));
```
```bash
Expand All @@ -322,10 +323,14 @@ Usage: help [options]
Options:
-t, --timeout <delay> timeout in seconds (default: one minute)
-d, --drink <size> drink cup size (choices: "small", "medium", "large")
-p, --port <number> port number (env: PORT)
-h, --help display help for command
$ extra --drink huge
error: option '-d, --drink <size>' argument 'huge' is invalid. Allowed choices are small, medium, large.
$ PORT=80 extra
Options: { timeout: 60, port: '80' }
```
### Custom option processing
Expand Down
2 changes: 1 addition & 1 deletion docs/deprecated.md
Expand Up @@ -39,7 +39,7 @@ The global Command object is exported as `program` from Commander v5, or import
const { program } = require('commander');
// or
const { Command } = require('commander');
comnst program = new Command()
const program = new Command()
```

- Removed from README in Commander v5.
Expand Down
38 changes: 38 additions & 0 deletions examples/options-env.js
@@ -0,0 +1,38 @@
#!/usr/bin/env node
// const { Command, Option } = require('commander'); // (normal include)
const { Command, Option } = require('../'); // include commander in git clone of commander repo
const program = new Command();

program.addOption(new Option('-p, --port <number>', 'specify port number')
.default(80)
.env('PORT')
);
program.addOption(new Option('-c, --colour', 'turn on colour output')
.env('COLOUR')
);
program.addOption(new Option('-C, --no-colour', 'turn off colour output')
.env('NO_COLOUR')
);
program.addOption(new Option('-s, --size <type>', 'specify size of drink')
.choices(['small', 'medium', 'large'])
.env('SIZE')
);

program.parse();
console.log(program.opts());

// Try the following:
// node options-env.js --help
//
// node options-env.js
// PORT=9001 node options-env.js
// PORT=9001 node options-env.js --port 123
//
// COLOUR= node options-env.js
// COLOUR= node options-env.js --no-colour
// NO_COLOUR= node options-env.js
// NO_COLOUR= node options-env.js --colour
//
// SIZE=small node options-env.js
// SIZE=enormous node options-env.js
// SIZE=enormous node options-env.js --size=large
19 changes: 11 additions & 8 deletions examples/options-extra.js
@@ -1,20 +1,23 @@
#!/usr/bin/env node

// This is used as an example in the README for extra option features.
// See also options-env.js for more extensive env examples.

// const commander = require('commander'); // (normal include)
const commander = require('../'); // include commander in git clone of commander repo
const program = new commander.Command();
// const { Command, Option } = require('commander'); // (normal include)
const { Command, Option } = require('../'); // include commander in git clone of commander repo
const program = new Command();

program
.addOption(new commander.Option('-s, --secret').hideHelp())
.addOption(new commander.Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
.addOption(new commander.Option('-d, --drink <size>', 'drink cup size').choices(['small', 'medium', 'large']));
.addOption(new Option('-s, --secret').hideHelp())
.addOption(new Option('-t, --timeout <delay>', 'timeout in seconds').default(60, 'one minute'))
.addOption(new Option('-d, --drink <size>', 'drink cup size').choices(['small', 'medium', 'large']))
.addOption(new Option('-p, --port <number>', 'port number').env('PORT'));

program.parse();

console.log('Options: ', program.opts());

// Try the following:
// node options-extra.js --help
// node options-extra.js --drink huge
// node options-extra.js --help
// node options-extra.js --drink huge
// PORT=80 node options-extra.js
64 changes: 54 additions & 10 deletions lib/command.js
Expand Up @@ -35,6 +35,7 @@ class Command extends EventEmitter {
this._scriptPath = null;
this._name = name || '';
this._optionValues = {};
this._optionValueSources = {}; // default < env < cli
this._storeOptionsAsProperties = false;
this._actionHandler = null;
this._executableHandler = false;
Expand Down Expand Up @@ -504,16 +505,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
}
// preassign only if we have a default
if (defaultValue !== undefined) {
this.setOptionValue(name, defaultValue);
this._setOptionValueWithSource(name, defaultValue, 'default');
}
}

// register the option
this.options.push(option);

// when it's passed assign the value
// and conditionally invoke the callback
this.on('option:' + oname, (val) => {
// handler for cli and env supplied values
const handleOptionValue = (val, invalidValueMessage, valueSource) => {
// Note: using closure to access lots of lexical scoped variables.
const oldValue = this.getOptionValue(name);

// custom processing
Expand All @@ -522,7 +523,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
val = option.parseArg(val, oldValue === undefined ? defaultValue : oldValue);
} catch (err) {
if (err.code === 'commander.invalidArgument') {
const message = `error: option '${option.flags}' argument '${val}' is invalid. ${err.message}`;
const message = `${invalidValueMessage} ${err.message}`;
this._displayError(err.exitCode, err.code, message);
}
throw err;
Expand All @@ -535,18 +536,28 @@ Expecting one of '${allowedValues.join("', '")}'`);
if (typeof oldValue === 'boolean' || typeof oldValue === 'undefined') {
// if no value, negate false, and we have a default, then use it!
if (val == null) {
this.setOptionValue(name, option.negate
? false
: defaultValue || true);
this._setOptionValueWithSource(name, option.negate ? false : defaultValue || true, valueSource);
} else {
this.setOptionValue(name, val);
this._setOptionValueWithSource(name, val, valueSource);
}
} else if (val !== null) {
// reassign
this.setOptionValue(name, option.negate ? false : val);
this._setOptionValueWithSource(name, option.negate ? false : val, valueSource);
}
};

this.on('option:' + oname, (val) => {
const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`;
handleOptionValue(val, invalidValueMessage, 'cli');
});

if (option.envVar) {
this.on('optionEnv:' + oname, (val) => {
const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`;
handleOptionValue(val, invalidValueMessage, 'env');
});
}

return this;
}

Expand Down Expand Up @@ -759,6 +770,14 @@ Expecting one of '${allowedValues.join("', '")}'`);
return this;
};

/**
* @api private
*/
_setOptionValueWithSource(key, value, source) {
this.setOptionValue(key, value);
this._optionValueSources[key] = source;
}

/**
* Get user arguments from implied or explicit arguments.
* Side-effects: set _scriptPath if args included script. Used for default program name, and subcommand searches.
Expand Down Expand Up @@ -1136,6 +1155,7 @@ Expecting one of '${allowedValues.join("', '")}'`);

_parseCommand(operands, unknown) {
const parsed = this.parseOptions(unknown);
this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env
operands = operands.concat(parsed.operands);
unknown = parsed.unknown;
this.args = operands.concat(unknown);
Expand Down Expand Up @@ -1416,6 +1436,30 @@ Expecting one of '${allowedValues.join("', '")}'`);
this._exit(exitCode, code, message);
}

/**
* Apply any option related environment variables, if option does
* not have a value from cli or client code.
*
* @api private
*/
_parseOptionsEnv() {
this.options.forEach((option) => {
if (option.envVar && option.envVar in process.env) {
const optionKey = option.attributeName();
// env is second lowest priority source, above default
if (this.getOptionValue(optionKey) === undefined || this._optionValueSources[optionKey] === 'default') {
if (option.required || option.optional) { // option can take a value
// keep very simple, optional always takes value
this.emit(`optionEnv:${option.name()}`, process.env[option.envVar]);
} else { // boolean
// keep very simple, only care that envVar defined and not the value
this.emit(`optionEnv:${option.name()}`);
}
}
}
});
}

/**
* Argument `name` is missing.
*
Expand Down
13 changes: 8 additions & 5 deletions lib/help.js
Expand Up @@ -234,21 +234,24 @@ class Help {
*/

optionDescription(option) {
if (option.negate) {
return option.description;
}
const extraInfo = [];
if (option.argChoices) {
// Some of these do not make sense for negated boolean and suppress for backwards compatibility.

if (option.argChoices && !option.negate) {
extraInfo.push(
// use stringify to match the display of the default value
`choices: ${option.argChoices.map((choice) => JSON.stringify(choice)).join(', ')}`);
}
if (option.defaultValue !== undefined) {
if (option.defaultValue !== undefined && !option.negate) {
extraInfo.push(`default: ${option.defaultValueDescription || JSON.stringify(option.defaultValue)}`);
}
if (option.envVar !== undefined) {
extraInfo.push(`env: ${option.envVar}`);
}
if (extraInfo.length > 0) {
return `${option.description} (${extraInfo.join(', ')})`;
}

return option.description;
};

Expand Down
14 changes: 14 additions & 0 deletions lib/option.js
Expand Up @@ -28,6 +28,7 @@ class Option {
}
this.defaultValue = undefined;
this.defaultValueDescription = undefined;
this.envVar = undefined;
this.parseArg = undefined;
this.hidden = false;
this.argChoices = undefined;
Expand All @@ -47,6 +48,19 @@ class Option {
return this;
};

/**
* Set environment variable to check for option value.
* Priority order of option values is default < env < cli
*
* @param {string} name
* @return {Option}
*/

env(name) {
this.envVar = name;
return this;
};

/**
* Set the custom handler for processing CLI option arguments into option values.
*
Expand Down
7 changes: 7 additions & 0 deletions tests/help.optionDescription.test.js
Expand Up @@ -24,6 +24,13 @@ describe('optionDescription', () => {
expect(helper.optionDescription(option)).toEqual('description (default: "default")');
});

test('when option has env then return description and env name', () => {
const description = 'description';
const option = new commander.Option('-a', description).env('ENV');
const helper = new commander.Help();
expect(helper.optionDescription(option)).toEqual('description (env: ENV)');
});

test('when option has default value description then return description and custom default description', () => {
const description = 'description';
const defaultValueDescription = 'custom';
Expand Down
6 changes: 6 additions & 0 deletions tests/option.chain.test.js
Expand Up @@ -30,4 +30,10 @@ describe('Option methods that should return this for chaining', () => {
const result = option.choices(['a']);
expect(result).toBe(option);
});

test('when call .env() then returns this', () => {
const option = new Option('-e,--example <value>');
const result = option.env('e');
expect(result).toBe(option);
});
});

0 comments on commit dd28741

Please sign in to comment.