diff --git a/lib/command.js b/lib/command.js index 3cfc3ba32..d904cfb00 100644 --- a/lib/command.js +++ b/lib/command.js @@ -57,6 +57,16 @@ class Command extends EventEmitter { this._showHelpAfterError = false; this._showSuggestionAfterError = true; + this._strictPriority = false; + this._priorities = [ + 'cli', + 'env', + 'config', // placeholder for configuration files + 'implied', // implied by others configs + 'default', // default option value + undefined // initial state, not touched yet + ]; + // see .configureOutput() for docs this._outputConfiguration = { writeOut: (str) => process.stdout.write(str), @@ -105,6 +115,8 @@ class Command extends EventEmitter { this._enablePositionalOptions = sourceCommand._enablePositionalOptions; this._showHelpAfterError = sourceCommand._showHelpAfterError; this._showSuggestionAfterError = sourceCommand._showSuggestionAfterError; + this._strictPriority = sourceCommand._strictPriority; + this._priorities = sourceCommand._priorities; return this; } @@ -412,6 +424,11 @@ Expecting one of '${allowedValues.join("', '")}'`); return this; } + strictPriority() { + this._strictPriority = true; + return this; + } + /** * Register callback to use as replacement for calling process.exit. * @@ -780,11 +797,7 @@ Expecting one of '${allowedValues.join("', '")}'`); */ setOptionValue(key, value) { - if (this._storeOptionsAsProperties) { - this[key] = value; - } else { - this._optionValues[key] = value; - } + this.setOptionValueWithSource(key, value); return this; } @@ -797,8 +810,13 @@ Expecting one of '${allowedValues.join("', '")}'`); * @return {Command} `this` command for chaining */ - setOptionValueWithSource(key, value, source) { - this.setOptionValue(key, value); + setOptionValueWithSource(key, value, source = null) { + if (this._strictPriority && !this._isNewSourcePreferred(source, this.getOptionValueSource(key))) return this; + if (this._storeOptionsAsProperties) { + this[key] = value; + } else { + this._optionValues[key] = value; + } this._optionValueSources[key] = source; return this; } @@ -1584,7 +1602,7 @@ Expecting one of '${allowedValues.join("', '")}'`); if (option.envVar && option.envVar in process.env) { const optionKey = option.attributeName(); // Priority check. Do not overwrite cli or options from unknown source (client-code). - if (this.getOptionValue(optionKey) === undefined || ['default', 'config', 'env'].includes(this.getOptionValueSource(optionKey))) { + if (this.getOptionValue(optionKey) === undefined || this._isNewSourcePreferred('env', this.getOptionValueSource(optionKey))) { 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]); @@ -1597,6 +1615,15 @@ Expecting one of '${allowedValues.join("', '")}'`); }); } + /** + * Check if newSource is preferred. + * + * @api private + */ + _isNewSourcePreferred(newSource, existingSource) { + return this._priorities.indexOf(newSource) <= this._priorities.indexOf(existingSource); + } + /** * Apply any implied option values, if option is undefined or default value. * @@ -1605,7 +1632,7 @@ Expecting one of '${allowedValues.join("', '")}'`); _parseOptionsImplied() { const dualHelper = new DualOptions(this.options); const hasCustomOptionValue = (optionKey) => { - return this.getOptionValue(optionKey) !== undefined && !['default', 'implied'].includes(this.getOptionValueSource(optionKey)); + return this.getOptionValue(optionKey) !== undefined && !this._isNewSourcePreferred('implied', this.getOptionValueSource(optionKey)); }; this.options .filter(option => (option.implied !== undefined) && diff --git a/typings/index.d.ts b/typings/index.d.ts index 9f3d8dff8..aa65b5f69 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -266,7 +266,7 @@ export interface OutputConfiguration { export type AddHelpTextPosition = 'beforeAll' | 'before' | 'after' | 'afterAll'; export type HookEvent = 'preSubcommand' | 'preAction' | 'postAction'; -export type OptionValueSource = 'default' | 'env' | 'config' | 'cli'; +export type OptionValueSource = 'default' | 'implied' | 'config' | 'env' | 'cli' | null; export interface OptionValues { [key: string]: any; @@ -595,7 +595,7 @@ export class Command { /** * Retrieve option value source. */ - getOptionValueSource(key: string): OptionValueSource; + getOptionValueSource(key: string): OptionValueSource | undefined; /** * Alter parsing of short flags with optional values. diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 94c0b53d1..7b8af12ea 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -172,7 +172,7 @@ expectType(program.setOptionValue('example', true)); expectType(program.setOptionValueWithSource('example', [], 'cli')); // getOptionValueSource -expectType(program.getOptionValueSource('example')); +expectType(program.getOptionValueSource('example')); // combineFlagAndOptionalValue expectType(program.combineFlagAndOptionalValue());