-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Clean up state from previous parse call when calling parse()
/ parseAsync()
#1919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
3c6ef71
b6d53b2
437291f
fbe911f
ebf672a
7fd0b22
940b111
52d5885
94e439d
274be1a
56eed63
4a7bc41
fcc97e2
36fa763
829655d
633e4e4
be00f59
b53703b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,14 +30,6 @@ class Command extends EventEmitter { | |
this._allowExcessArguments = true; | ||
/** @type {Argument[]} */ | ||
this._args = []; | ||
/** @type {string[]} */ | ||
this.args = []; // cli args with options removed | ||
this.rawArgs = []; | ||
this.processedArgs = []; // like .args but after custom processing and collecting variadic | ||
this._scriptPath = null; | ||
this._name = name || ''; | ||
this._optionValues = {}; | ||
this._optionValueSources = {}; // default, env, cli etc | ||
this._storeOptionsAsProperties = false; | ||
this._actionHandler = null; | ||
this._executableHandler = false; | ||
|
@@ -77,6 +69,28 @@ class Command extends EventEmitter { | |
this._helpCommandnameAndArgs = 'help [command]'; | ||
this._helpCommandDescription = 'display help for command'; | ||
this._helpConfiguration = {}; | ||
|
||
this._unprocessedName = name || ''; | ||
this._persistentOptionValues = {}; | ||
this._persistentOptionValueSources = {}; | ||
this.resetParseState(); | ||
|
||
/** @type {boolean | undefined} */ | ||
this._asyncParsing = undefined; | ||
} | ||
|
||
resetParseState() { | ||
/** @type {string[]} */ | ||
this.args = []; // cli args with options removed | ||
this.rawArgs = []; | ||
this.processedArgs = []; // like .args but after custom processing and collecting variadic | ||
this._scriptPath = null; | ||
|
||
this._name = this._unprocessedName; | ||
this._optionValues = Object.assign({}, this._persistentOptionValues); | ||
this._optionValueSources = Object.assign( | ||
{}, this._persistentOptionValueSources | ||
); // default, env, cli etc | ||
} | ||
|
||
/** | ||
|
@@ -515,10 +529,10 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
// --no-foo is special and defaults foo to true, unless a --foo option is already defined | ||
const positiveLongFlag = option.long.replace(/^--no-/, '--'); | ||
if (!this._findOption(positiveLongFlag)) { | ||
this.setOptionValueWithSource(name, option.defaultValue === undefined ? true : option.defaultValue, 'default'); | ||
this._setPersistentOptionValueWithSource(name, option.defaultValue === undefined ? true : option.defaultValue, 'default'); | ||
} | ||
} else if (option.defaultValue !== undefined) { | ||
this.setOptionValueWithSource(name, option.defaultValue, 'default'); | ||
this._setPersistentOptionValueWithSource(name, option.defaultValue, 'default'); | ||
} | ||
|
||
// register the option | ||
|
@@ -558,7 +572,7 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
val = ''; // not normal, parseArg might have failed or be a mock function for testing | ||
} | ||
} | ||
this.setOptionValueWithSource(name, val, valueSource); | ||
this._setNonPersistentOptionValueWithSource(name, val, valueSource); | ||
}; | ||
|
||
this.on('option:' + oname, (val) => { | ||
|
@@ -780,34 +794,61 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
*/ | ||
|
||
setOptionValue(key, value) { | ||
return this.setOptionValueWithSource(key, value, undefined); | ||
return this.setOptionValueWithSource(key, value); | ||
} | ||
|
||
/** | ||
* Store option value and where the value came from. | ||
* | ||
* @param {string} key | ||
* @param {Object} value | ||
* @param {string} source - expected values are default/config/env/cli/implied | ||
* @param {string} [source] | ||
* @return {Command} `this` command for chaining | ||
*/ | ||
|
||
setOptionValueWithSource(key, value, source) { | ||
const set = this._asyncParsing === undefined | ||
? this._setPersistentOptionValueWithSource | ||
: this._setNonPersistentOptionValueWithSource; | ||
set(key, value, source); | ||
return this; | ||
} | ||
|
||
/** | ||
* @param {string} key | ||
* @param {Object} value | ||
* @param {string} [source] | ||
* @api private | ||
*/ | ||
|
||
_setPersistentOptionValueWithSource(key, value, source) { | ||
this._setNonPersistentOptionValueWithSource(key, value, source); | ||
this._persistentOptionValues[key] = value; | ||
this._persistentOptionValueSources[key] = source; | ||
} | ||
|
||
/** | ||
* @param {string} key | ||
* @param {Object} value | ||
* @param {string} [source] | ||
* @api private | ||
*/ | ||
|
||
_setNonPersistentOptionValueWithSource(key, value, source) { | ||
if (this._storeOptionsAsProperties) { | ||
this[key] = value; | ||
} else { | ||
this._optionValues[key] = value; | ||
} | ||
this._optionValueSources[key] = source; | ||
return this; | ||
} | ||
|
||
/** | ||
* Get source of option value. | ||
* Expected values are default | config | env | cli | implied | ||
* | ||
* @param {string} key | ||
* @return {string} | ||
* @return {string | undefined} | ||
*/ | ||
|
||
getOptionValueSource(key) { | ||
|
@@ -819,7 +860,7 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
* Expected values are default | config | env | cli | implied | ||
* | ||
* @param {string} key | ||
* @return {string} | ||
* @return {string | undefined} | ||
*/ | ||
|
||
getOptionValueSourceWithGlobals(key) { | ||
|
@@ -887,6 +928,22 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
return userArgs; | ||
} | ||
|
||
/** | ||
* @param {boolean} async | ||
* @param {Function} userArgsCallback | ||
* @param {string[]} [argv] | ||
* @param {Object} [parseOptions] | ||
* @param {string} [parseOptions.from] | ||
* @return {Command|Promise} | ||
* @api private | ||
*/ | ||
|
||
_parseSubroutine(async, userArgsCallback, argv, parseOptions) { | ||
this.resetParseState(); | ||
const userArgs = this._prepareUserArgs(argv, parseOptions); | ||
return userArgsCallback(userArgs); | ||
} | ||
|
||
/** | ||
* Parse `argv`, setting options and invoking commands when defined. | ||
* | ||
|
@@ -905,10 +962,10 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
*/ | ||
|
||
parse(argv, parseOptions) { | ||
const userArgs = this._prepareUserArgs(argv, parseOptions); | ||
this._parseCommand([], userArgs); | ||
|
||
return this; | ||
return this._parseSubroutine(false, (userArgs) => { | ||
this._parseCommand([], userArgs); | ||
return this; | ||
}, argv, parseOptions); | ||
} | ||
|
||
/** | ||
|
@@ -931,10 +988,10 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
*/ | ||
|
||
async parseAsync(argv, parseOptions) { | ||
const userArgs = this._prepareUserArgs(argv, parseOptions); | ||
await this._parseCommand([], userArgs); | ||
|
||
return this; | ||
return this._parseSubroutine(true, async(userArgs) => { | ||
await this._parseCommand([], userArgs); | ||
return this; | ||
}, argv, parseOptions); | ||
} | ||
|
||
/** | ||
|
@@ -1078,7 +1135,7 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
if (subCommand._executableHandler) { | ||
this._executeSubCommand(subCommand, operands.concat(unknown)); | ||
} else { | ||
return subCommand._parseCommand(operands, unknown); | ||
return subCommand._parseCommand(operands, unknown, this._asyncParsing); | ||
} | ||
}); | ||
return hookResult; | ||
|
@@ -1256,81 +1313,87 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
* @api private | ||
*/ | ||
|
||
_parseCommand(operands, unknown) { | ||
const parsed = this.parseOptions(unknown); | ||
this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env | ||
this._parseOptionsImplied(); | ||
operands = operands.concat(parsed.operands); | ||
unknown = parsed.unknown; | ||
this.args = operands.concat(unknown); | ||
|
||
if (operands && this._findCommand(operands[0])) { | ||
return this._dispatchSubcommand(operands[0], operands.slice(1), unknown); | ||
} | ||
if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { | ||
return this._dispatchHelpCommand(operands[1]); | ||
} | ||
if (this._defaultCommandName) { | ||
outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command | ||
return this._dispatchSubcommand(this._defaultCommandName, operands, unknown); | ||
} | ||
if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { | ||
// probably missing subcommand and no handler, user needs help (and exit) | ||
this.help({ error: true }); | ||
} | ||
_parseCommand(operands, unknown, async) { | ||
this._asyncParsing = async; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, for motivation see commit message of 94e439d. #1917 does not use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In my opinion it is ok to keep them in mind, and helpful to warn about potential merge issues. (Even ok in my opinion to suggest it should land after another PR, but then be prepared for a rewrite if that other one gets rejected!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
outputHelpIfRequested(this, parsed.unknown); | ||
this._checkForMissingMandatoryOptions(); | ||
this._checkForConflictingOptions(); | ||
try { | ||
const parsed = this.parseOptions(unknown); | ||
this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env | ||
this._parseOptionsImplied(); | ||
operands = operands.concat(parsed.operands); | ||
unknown = parsed.unknown; | ||
this.args = operands.concat(unknown); | ||
|
||
// We do not always call this check to avoid masking a "better" error, like unknown command. | ||
const checkForUnknownOptions = () => { | ||
if (parsed.unknown.length > 0) { | ||
this.unknownOption(parsed.unknown[0]); | ||
if (operands && this._findCommand(operands[0])) { | ||
return this._dispatchSubcommand(operands[0], operands.slice(1), unknown); | ||
} | ||
}; | ||
|
||
const commandEvent = `command:${this.name()}`; | ||
if (this._actionHandler) { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
|
||
let actionResult; | ||
actionResult = this._chainOrCallHooks(actionResult, 'preAction'); | ||
actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs)); | ||
if (this.parent) { | ||
actionResult = this._chainOrCall(actionResult, () => { | ||
this.parent.emit(commandEvent, operands, unknown); // legacy | ||
}); | ||
if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) { | ||
return this._dispatchHelpCommand(operands[1]); | ||
} | ||
actionResult = this._chainOrCallHooks(actionResult, 'postAction'); | ||
return actionResult; | ||
} | ||
if (this.parent && this.parent.listenerCount(commandEvent)) { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
this.parent.emit(commandEvent, operands, unknown); // legacy | ||
} else if (operands.length) { | ||
if (this._findCommand('*')) { // legacy default command | ||
return this._dispatchSubcommand('*', operands, unknown); | ||
if (this._defaultCommandName) { | ||
outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command | ||
return this._dispatchSubcommand(this._defaultCommandName, operands, unknown); | ||
} | ||
if (this.listenerCount('command:*')) { | ||
// skip option check, emit event for possible misspelling suggestion | ||
this.emit('command:*', operands, unknown); | ||
if (this.commands.length && this.args.length === 0 && !this._actionHandler && !this._defaultCommandName) { | ||
// probably missing subcommand and no handler, user needs help (and exit) | ||
this.help({ error: true }); | ||
} | ||
|
||
outputHelpIfRequested(this, parsed.unknown); | ||
this._checkForMissingMandatoryOptions(); | ||
this._checkForConflictingOptions(); | ||
|
||
// We do not always call this check to avoid masking a "better" error, like unknown command. | ||
const checkForUnknownOptions = () => { | ||
if (parsed.unknown.length > 0) { | ||
this.unknownOption(parsed.unknown[0]); | ||
} | ||
}; | ||
|
||
const commandEvent = `command:${this.name()}`; | ||
if (this._actionHandler) { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
|
||
let actionResult; | ||
actionResult = this._chainOrCallHooks(actionResult, 'preAction'); | ||
actionResult = this._chainOrCall(actionResult, () => this._actionHandler(this.processedArgs)); | ||
if (this.parent) { | ||
actionResult = this._chainOrCall(actionResult, () => { | ||
this.parent.emit(commandEvent, operands, unknown); // legacy | ||
}); | ||
} | ||
actionResult = this._chainOrCallHooks(actionResult, 'postAction'); | ||
return actionResult; | ||
} | ||
if (this.parent && this.parent.listenerCount(commandEvent)) { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
this.parent.emit(commandEvent, operands, unknown); // legacy | ||
} else if (operands.length) { | ||
if (this._findCommand('*')) { // legacy default command | ||
return this._dispatchSubcommand('*', operands, unknown); | ||
} | ||
if (this.listenerCount('command:*')) { | ||
// skip option check, emit event for possible misspelling suggestion | ||
this.emit('command:*', operands, unknown); | ||
} else if (this.commands.length) { | ||
this.unknownCommand(); | ||
} else { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
} | ||
} else if (this.commands.length) { | ||
this.unknownCommand(); | ||
checkForUnknownOptions(); | ||
// This command has subcommands and nothing hooked up at this level, so display help (and exit). | ||
this.help({ error: true }); | ||
} else { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
// fall through for caller to handle after calling .parse() | ||
} | ||
} else if (this.commands.length) { | ||
checkForUnknownOptions(); | ||
// This command has subcommands and nothing hooked up at this level, so display help (and exit). | ||
this.help({ error: true }); | ||
} else { | ||
checkForUnknownOptions(); | ||
this._processArguments(); | ||
// fall through for caller to handle after calling .parse() | ||
} finally { | ||
this._asyncParsing = undefined; | ||
} | ||
} | ||
|
||
|
@@ -1650,7 +1713,9 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
Object.keys(option.implied) | ||
.filter(impliedKey => !hasCustomOptionValue(impliedKey)) | ||
.forEach(impliedKey => { | ||
this.setOptionValueWithSource(impliedKey, option.implied[impliedKey], 'implied'); | ||
this._setNonPersistentOptionValueWithSource( | ||
impliedKey, option.implied[impliedKey], 'implied' | ||
); | ||
}); | ||
}); | ||
} | ||
|
@@ -1932,7 +1997,7 @@ Expecting one of '${allowedValues.join("', '")}'`); | |
|
||
name(str) { | ||
if (str === undefined) return this._name; | ||
this._name = str; | ||
this._name = this._unprocessedName = str; | ||
return this; | ||
} | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.