diff --git a/Readme.md b/Readme.md index afe042d5e..aa4f42b59 100644 --- a/Readme.md +++ b/Readme.md @@ -11,7 +11,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Commander.js](#commanderjs) - [Installation](#installation) - - [Declaring _program_ variable](#declaring-program-variable) + - [Declaring program variable](#declaring-program-variable) - [Options](#options) - [Common option types, boolean and value](#common-option-types-boolean-and-value) - [Default option value](#default-option-value) @@ -33,7 +33,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Bits and pieces](#bits-and-pieces) - [Avoiding option name clashes](#avoiding-option-name-clashes) - [TypeScript](#typescript) - - [Node options such as `--harmony`](#node-options-such-as---harmony) + - [Node options such as --harmony](#node-options-such-as---harmony) - [Node debugging](#node-debugging) - [Override exit handling](#override-exit-handling) - [Examples](#examples) @@ -377,6 +377,19 @@ program program.parse(process.argv) ``` +You may supply an `async` action handler, in which case you call `.parseAsync` rather than `.parse`. + +```js +async function run() { /* code goes here */ } + +async function main() { + program + .command('run') + .action(run); + await program.parseAsync(process.argv); +} +``` + A command's options on the command line are validated when the command is used. Any unknown options will be reported as an error. However, if an action-based command does not define an action, then the options are not validated. Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. diff --git a/index.js b/index.js index b7e35778a..37d20cc5e 100644 --- a/index.js +++ b/index.js @@ -129,6 +129,7 @@ function Command(name) { this._optionValues = {}; this._storeOptionsAsProperties = true; // backwards compatible by default this._passCommandToAction = true; // backwards compatible by default + this._actionResults = []; this._helpFlags = '-h, --help'; this._helpDescription = 'output usage information'; @@ -366,7 +367,13 @@ Command.prototype.action = function(fn) { actionArgs.push(args.slice(expectedArgsCount)); } - fn.apply(self, actionArgs); + const actionResult = fn.apply(self, actionArgs); + // Remember result in case it is async. Assume parseAsync getting called on root. + let rootCommand = self; + while (rootCommand.parent) { + rootCommand = rootCommand.parent; + } + rootCommand._actionResults.push(actionResult); }; var parent = this.parent || this; var name = parent === this ? '*' : this._name; @@ -604,7 +611,7 @@ Command.prototype._getOptionValue = function(key) { }; /** - * Parse `argv`, settings options and invoking commands when defined. + * Parse `argv`, setting options and invoking commands when defined. * * @param {Array} argv * @return {Command} for chaining @@ -688,6 +695,20 @@ Command.prototype.parse = function(argv) { return result; }; +/** + * Parse `argv`, setting options and invoking commands when defined. + * + * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. + * + * @param {Array} argv + * @return {Promise} + * @api public + */ +Command.prototype.parseAsync = function(argv) { + this.parse(argv); + return Promise.all(this._actionResults); +}; + /** * Execute a sub-command executable. * diff --git a/tests/command.action.test.js b/tests/command.action.test.js index fa0f85c35..59469be9b 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -88,3 +88,19 @@ test('when .action on program with subcommand and program argument then program expect(actionMock).toHaveBeenCalledWith('a', program); }); + +test('when action is async then can await parseAsync', async() => { + let asyncFinished = false; + async function delay() { + await new Promise(resolve => setTimeout(resolve, 100)); + asyncFinished = true; + }; + const program = new commander.Command(); + program + .action(delay); + + const later = program.parseAsync(['node', 'test']); + expect(asyncFinished).toBe(false); + await later; + expect(asyncFinished).toBe(true); +}); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 9d05be47f..7661bcf0e 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -133,4 +133,10 @@ program.exitOverride((err):void => { program.parse(process.argv); +program.parseAsync(process.argv).then(() => { + console.log('parseAsync success'); +}).catch(err => { + console.log('parseAsync failed'); +}); + console.log('stuff'); diff --git a/typings/index.d.ts b/typings/index.d.ts index 2ed0fa6c4..d1a89d8c2 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -197,12 +197,21 @@ declare namespace commander { allowUnknownOption(arg?: boolean): Command; /** - * Parse `argv`, settings options and invoking commands when defined. + * Parse `argv`, setting options and invoking commands when defined. * - * @returns {Command} for chaining + * @returns Command for chaining */ parse(argv: string[]): Command; + /** + * Parse `argv`, setting options and invoking commands when defined. + * + * Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise. + * + * @returns Promise + */ + parseAsync(argv: string[]): Promise; + /** * Parse options from `argv` returning `argv` void of these options. */