Skip to content
Closed
151 changes: 112 additions & 39 deletions lib/command.js
Original file line number Diff line number Diff line change
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;