commander
Advanced tools
Comparing version 8.1.0 to 8.2.0
@@ -10,2 +10,3 @@ const EventEmitter = require('events').EventEmitter; | ||
const { Option, splitOptionFlags } = require('./option.js'); | ||
const { suggestSimilar } = require('./suggestSimilar'); | ||
@@ -39,2 +40,3 @@ // @ts-check | ||
this._optionValues = {}; | ||
this._optionValueSources = {}; // default < env < cli | ||
this._storeOptionsAsProperties = false; | ||
@@ -55,2 +57,3 @@ this._actionHandler = null; | ||
this._showHelpAfterError = false; | ||
this._showSuggestionAfterError = false; | ||
@@ -104,2 +107,3 @@ // see .configureOutput() for docs | ||
this._showHelpAfterError = sourceCommand._showHelpAfterError; | ||
this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError; | ||
@@ -240,2 +244,13 @@ return this; | ||
/** | ||
* Display suggestion of similar commands for unknown commands, or options for unknown options. | ||
* | ||
* @param {boolean} [displaySuggestion] | ||
* @return {Command} `this` command for chaining | ||
*/ | ||
showSuggestionAfterError(displaySuggestion = true) { | ||
this._showSuggestionAfterError = !!displaySuggestion; | ||
return this; | ||
} | ||
/** | ||
* Add a prepared subcommand. | ||
@@ -520,3 +535,3 @@ * | ||
if (defaultValue !== undefined) { | ||
this.setOptionValue(name, defaultValue); | ||
this._setOptionValueWithSource(name, defaultValue, 'default'); | ||
} | ||
@@ -528,5 +543,5 @@ } | ||
// 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); | ||
@@ -540,3 +555,3 @@ | ||
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); | ||
@@ -554,14 +569,24 @@ } | ||
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; | ||
@@ -780,2 +805,10 @@ } | ||
/** | ||
* @api private | ||
*/ | ||
_setOptionValueWithSource(key, value, source) { | ||
this.setOptionValue(key, value); | ||
this._optionValueSources[key] = source; | ||
} | ||
/** | ||
* Get user arguments implied or explicit arguments. | ||
@@ -1144,2 +1177,3 @@ * Side-effects: set _scriptPath if args included application, and use that to set implicit command name. | ||
const parsed = this.parseOptions(unknown); | ||
this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env | ||
operands = operands.concat(parsed.operands); | ||
@@ -1207,2 +1241,3 @@ unknown = parsed.unknown; | ||
} else if (this.commands.length) { | ||
checkForUnknownOptions(); | ||
// This command has subcommands and nothing hooked up at this level, so display help (and exit). | ||
@@ -1427,2 +1462,26 @@ this.help({ error: true }); | ||
/** | ||
* 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. | ||
@@ -1472,3 +1531,19 @@ * | ||
if (this._allowUnknownOption) return; | ||
const message = `error: unknown option '${flag}'`; | ||
let suggestion = ''; | ||
if (flag.startsWith('--') && this._showSuggestionAfterError) { | ||
// Looping to pick up the global options too | ||
let candidateFlags = []; | ||
let command = this; | ||
do { | ||
const moreFlags = command.createHelp().visibleOptions(command) | ||
.filter(option => option.long) | ||
.map(option => option.long); | ||
candidateFlags = candidateFlags.concat(moreFlags); | ||
command = command.parent; | ||
} while (command && !command._enablePositionalOptions); | ||
suggestion = suggestSimilar(flag, candidateFlags); | ||
} | ||
const message = `error: unknown option '${flag}'${suggestion}`; | ||
this._displayError(1, 'commander.unknownOption', message); | ||
@@ -1501,3 +1576,16 @@ }; | ||
unknownCommand() { | ||
const message = `error: unknown command '${this.args[0]}'`; | ||
const unknownName = this.args[0]; | ||
let suggestion = ''; | ||
if (this._showSuggestionAfterError) { | ||
const candidateNames = []; | ||
this.createHelp().visibleCommands(this).forEach((command) => { | ||
candidateNames.push(command.name()); | ||
// just visible alias | ||
if (command.alias()) candidateNames.push(command.alias()); | ||
}); | ||
suggestion = suggestSimilar(unknownName, candidateNames); | ||
} | ||
const message = `error: unknown command '${unknownName}'${suggestion}`; | ||
this._displayError(1, 'commander.unknownCommand', message); | ||
@@ -1504,0 +1592,0 @@ }; |
@@ -237,7 +237,6 @@ const { humanReadableArgName } = require('./argument.js'); | ||
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( | ||
@@ -247,8 +246,12 @@ // use stringify to match the display of the default value | ||
} | ||
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; | ||
@@ -255,0 +258,0 @@ }; |
@@ -31,2 +31,3 @@ const { InvalidArgumentError } = require('./error.js'); | ||
this.defaultValueDescription = undefined; | ||
this.envVar = undefined; | ||
this.parseArg = undefined; | ||
@@ -52,2 +53,15 @@ this.hidden = false; | ||
/** | ||
* 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. | ||
@@ -54,0 +68,0 @@ * |
{ | ||
"name": "commander", | ||
"version": "8.1.0", | ||
"version": "8.2.0", | ||
"description": "the complete solution for node.js command-line programs", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -96,3 +96,2 @@ # Commander.js | ||
## Options | ||
@@ -312,3 +311,3 @@ | ||
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) | ||
@@ -319,3 +318,4 @@ ```js | ||
.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')); | ||
``` | ||
@@ -330,2 +330,3 @@ | ||
-d, --drink <size> drink cup size (choices: "small", "medium", "large") | ||
-p, --port <number> port number (env: PORT) | ||
-h, --help display help for command | ||
@@ -335,2 +336,5 @@ | ||
error: option '-d, --drink <size>' argument 'huge' is invalid. Allowed choices are small, medium, large. | ||
$ PORT=80 extra | ||
Options: { timeout: 60, port: '80' } | ||
``` | ||
@@ -696,2 +700,14 @@ | ||
You can also show suggestions after an error for an unknown command or option. | ||
```js | ||
program.showSuggestionAfterError(); | ||
``` | ||
```sh | ||
$ pizza --hepl | ||
error: unknown option '--hepl' | ||
(Did you mean --help?) | ||
``` | ||
### Display help from code | ||
@@ -912,3 +928,2 @@ | ||
```js | ||
@@ -915,0 +930,0 @@ function errorColor(str) { |
@@ -47,4 +47,4 @@ // Type definitions for commander | ||
/** | ||
* Return argument name. | ||
*/ | ||
* Return argument name. | ||
*/ | ||
name(): string; | ||
@@ -55,3 +55,3 @@ | ||
*/ | ||
default(value: unknown, description?: string): this; | ||
default(value: unknown, description?: string): this; | ||
@@ -61,3 +61,3 @@ /** | ||
*/ | ||
argParser<T>(fn: (value: string, previous: T) => T): this; | ||
argParser<T>(fn: (value: string, previous: T) => T): this; | ||
@@ -67,3 +67,3 @@ /** | ||
*/ | ||
choices(values: string[]): this; | ||
choices(values: string[]): this; | ||
@@ -73,3 +73,3 @@ /** | ||
*/ | ||
argRequired(): this; | ||
argRequired(): this; | ||
@@ -108,2 +108,8 @@ /** | ||
/** | ||
* Set environment variable to check for option value. | ||
* Priority order of option values is default < env < cli | ||
*/ | ||
env(name: string): this; | ||
/** | ||
* Calculate the full description, including defaultValue etc. | ||
@@ -129,8 +135,2 @@ */ | ||
/** | ||
* Validation of option argument failed. | ||
* Intended for use from custom argument processing functions. | ||
*/ | ||
argumentRejected(messsage: string): never; | ||
/** | ||
* Only allow option value to be one of choices. | ||
@@ -416,3 +416,3 @@ */ | ||
*/ | ||
copyInheritedSettings(sourceCommand: Command): this; | ||
copyInheritedSettings(sourceCommand: Command): this; | ||
@@ -425,2 +425,7 @@ /** | ||
/** | ||
* Display suggestion of similar commands for unknown commands, or options for unknown options. | ||
*/ | ||
showSuggestionAfterError(displaySuggestion?: boolean): this; | ||
/** | ||
* Register callback `fn` for the command. | ||
@@ -427,0 +432,0 @@ * |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
150526
13
3222
1022
3