Skip to content
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

Feature/async parse args experiment #1913

Closed
151 changes: 112 additions & 39 deletions lib/command.js
Expand Up @@ -535,15 +535,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
// custom processing
const oldValue = this.getOptionValue(name);
if (val !== null && option.parseArg) {
try {
val = option.parseArg(val, oldValue);
} catch (err) {
if (err.code === 'commander.invalidArgument') {
const message = `${invalidValueMessage} ${err.message}`;
this.error(message, { exitCode: err.exitCode, code: err.code });
}
throw err;
}
val = this._parseArg(option, val, oldValue, invalidValueMessage);
} else if (val !== null && option.variadic) {
val = option._concatValue(val, oldValue);
}
Expand Down Expand Up @@ -1072,16 +1064,16 @@ Expecting one of '${allowedValues.join("', '")}'`);
const subCommand = this._findCommand(commandName);
if (!subCommand) this.help({ error: true });

let hookResult;
hookResult = this._chainOrCallSubCommandHook(hookResult, subCommand, 'preSubcommand');
hookResult = this._chainOrCall(hookResult, () => {
let chain = this._settleOptionPromises();
chain = this._chainOrCallSubCommandHook(chain, subCommand, 'preSubcommand');
chain = this._chainOrCall(chain, () => {
if (subCommand._executableHandler) {
this._executeSubCommand(subCommand, operands.concat(unknown));
} else {
return subCommand._parseCommand(operands, unknown);
}
});
return hookResult;
return chain;
}

/**
Expand Down Expand Up @@ -1134,18 +1126,10 @@ Expecting one of '${allowedValues.join("', '")}'`);

_processArguments() {
const myParseArg = (argument, value, previous) => {
// Extra processing for nice error message on parsing failure.
let parsedValue = value;
if (value !== null && argument.parseArg) {
try {
parsedValue = argument.parseArg(value, previous);
} catch (err) {
if (err.code === 'commander.invalidArgument') {
const message = `error: command-argument value '${value}' is invalid for argument '${argument.name()}'. ${err.message}`;
this.error(message, { exitCode: err.exitCode, code: err.code });
}
throw err;
}
const errorMessage = `error: command-argument value '${value}' is invalid for argument '${argument.name()}'.`;
parsedValue = this._parseArg(argument, value, previous, errorMessage);
}
return parsedValue;
};
Expand Down Expand Up @@ -1176,6 +1160,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
processedArgs[index] = value;
});
this.processedArgs = processedArgs;
return this._settleArgumentPromises();
}

/**
Expand All @@ -1188,8 +1173,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/

_chainOrCall(promise, fn) {
// thenable
if (promise && promise.then && typeof promise.then === 'function') {
if (thenable(promise)) {
// already have a promise, chain callback
return promise.then(() => fn());
}
Expand Down Expand Up @@ -1249,6 +1233,83 @@ Expecting one of '${allowedValues.join("', '")}'`);
return result;
}

/**
* Call parseArgs with extra handling:
* - custom error message if parseArgs throws 'commander.invalidArgument'
* - if the previous value is a promise, chain the call to resolve the promise before parsing
*
* @param {Option | Argument} target
* @param {string} value
* @param {Promise<any> | any} previous
* @param {string} invalidArgumentMessage
* @api private
*/

_parseArg(target, value, previous, invalidArgumentMessage) {
const refineError = (err) => {
if (err.code === 'commander.invalidArgument') {
const message = `${invalidArgumentMessage} ${err.message}`;
this.error(message, { exitCode: err.exitCode, code: err.code });
}
throw err;
};

let result;
if (thenable(previous)) {
result = previous.then(resolvedPrevious => {
let innerResult = target.parseArg(value, resolvedPrevious);
if (thenable(innerResult)) innerResult = innerResult.then(null, refineError); // .catch
return innerResult;
});
} else {
try {
result = target.parseArg(value, previous);
if (thenable(result)) result = result.then(null, refineError); // .catch
} catch (err) {
refineError(err);
}
}
return result;
}

/**
* @api private
*/

_settleOptionPromises() {
// Look through the options for promises from async parseArgs (or other sources).
const promises = Object.entries(this.opts())
.filter(([key, maybePromise]) => thenable(maybePromise))
.map(([key, promise]) => {
return promise.then(value => {
this.setOptionValueWithSource(key, value, this.getOptionValueSource(key));
});
});

if (promises.length > 0) {
return Promise.all(promises);
}
}

/**
* @api private
*/

_settleArgumentPromises() {
// Look through the arguments for promises from async parseArgs (or other sources).
const promises = this.processedArgs
.filter(thenable)
.map((promise, index) => {
return promise.then(value => {
this.processedArgs[index] = value;
});
});

if (promises.length > 0) {
return Promise.all(promises);
}
}

/**
* Process arguments in context of this command.
* Returns action result, in case it is a promise.
Expand Down Expand Up @@ -1289,27 +1350,28 @@ Expecting one of '${allowedValues.join("', '")}'`);
this.unknownOption(parsed.unknown[0]);
}
};
const settleAndProcessArguments = () => {
const chain = this._settleOptionPromises();
return this._chainOrCall(chain, () => this._processArguments());
};

let chain;
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));
chain = settleAndProcessArguments();
chain = this._chainOrCallHooks(chain, 'preAction');
chain = this._chainOrCall(chain, () => this._actionHandler(this.processedArgs));
if (this.parent) {
actionResult = this._chainOrCall(actionResult, () => {
chain = this._chainOrCall(chain, () => {
this.parent.emit(commandEvent, operands, unknown); // legacy
});
}
actionResult = this._chainOrCallHooks(actionResult, 'postAction');
return actionResult;
}
if (this.parent && this.parent.listenerCount(commandEvent)) {
chain = this._chainOrCallHooks(chain, 'postAction');
} else if (this.parent && this.parent.listenerCount(commandEvent)) {
checkForUnknownOptions();
this._processArguments();
this.parent.emit(commandEvent, operands, unknown); // legacy
chain = settleAndProcessArguments();
chain = this._chainOrCall(chain, () => { this.parent.emit(commandEvent, operands, unknown); }); // Legacy
} else if (operands.length) {
if (this._findCommand('*')) { // legacy default command
return this._dispatchSubcommand('*', operands, unknown);
Expand All @@ -1321,17 +1383,18 @@ Expecting one of '${allowedValues.join("', '")}'`);
this.unknownCommand();
} else {
checkForUnknownOptions();
this._processArguments();
chain = settleAndProcessArguments();
}
} 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();
chain = settleAndProcessArguments();
// fall through for caller to handle after calling .parse()
}
return chain;
}

/**
Expand Down Expand Up @@ -2193,4 +2256,14 @@ function getCommandAndParents(startCommand) {
return result;
}

/**
* @param {Object} obj
* @returns {boolean}
* @api private
*/

function thenable(obj) {
return !!(obj && obj.then && typeof obj.then === 'function');
}

exports.Command = Command;