Skip to content

Commit

Permalink
Add parseAsync (#1118)
Browse files Browse the repository at this point in the history
* First cut at parseAsync

* Add await parseSync test

* Fix typo in JSDoc

* Add parseAsync to README

Some noise in TOC due to changes in plugin which maintains TOC.
  • Loading branch information
shadowspawn committed Jan 5, 2020
1 parent 1d9cc72 commit 7bcf117
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 6 deletions.
17 changes: 15 additions & 2 deletions Readme.md
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 23 additions & 2 deletions index.js
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
16 changes: 16 additions & 0 deletions tests/command.action.test.js
Expand Up @@ -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);
});
6 changes: 6 additions & 0 deletions typings/commander-tests.ts
Expand Up @@ -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');
13 changes: 11 additions & 2 deletions typings/index.d.ts
Expand Up @@ -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<any>;

/**
* Parse options from `argv` returning `argv` void of these options.
*/
Expand Down

0 comments on commit 7bcf117

Please sign in to comment.