Skip to content

Commit

Permalink
Add override to allow throw instead of process.exit (#1040)
Browse files Browse the repository at this point in the history
* Add TypeScript for exitOverride

* Switch from _exitOveride to exitOveride

* Add exitOverride to README
  • Loading branch information
shadowspawn committed Sep 20, 2019
1 parent d7f9cd4 commit 8df692e
Show file tree
Hide file tree
Showing 12 changed files with 90 additions and 37 deletions.
19 changes: 19 additions & 0 deletions Readme.md
Expand Up @@ -31,6 +31,7 @@ The complete solution for [node.js](http://nodejs.org) command-line interfaces,
- [TypeScript](#typescript)
- [Node options such as `--harmony`](#node-options-such-as---harmony)
- [Node debugging](#node-debugging)
- [Override exit handling](#override-exit-handling)
- [Examples](#examples)
- [License](#license)
- [Support](#support)
Expand Down Expand Up @@ -554,6 +555,24 @@ You can enable `--harmony` option in two ways:
If you are using the node inspector for [debugging](https://nodejs.org/en/docs/guides/debugging-getting-started/) git-style executable (sub)commands using `node --inspect` et al,
the inspector port is incremented by 1 for the spawned subcommand.
### Override exit handling
By default Commander calls `process.exit` when it detects errors, or after displaying the help or version. You can override
this behaviour and optionally supply a callback. The default override throws a `CommanderError`.
The override callback is passed a `CommanderError` with properties `exitCode` number, `code` string, and `message`. The default override behaviour is to throw the error, except for async handling of executable subcommand completion which carries on. The normal display of error messages or version or help
is not affected by the override which is called after the display.
``` js
program.exitOverride();
try {
program.parse(process.argv);
} catch (err) {
// custom processing...
}
```
## Examples
```js
Expand Down
6 changes: 3 additions & 3 deletions index.js
Expand Up @@ -255,14 +255,14 @@ Command.prototype.parseExpectedArgs = function(args) {
};

/**
* Register callback `fn` to use as replacement for calling process.exit.
* Register callback to use as replacement for calling process.exit.
*
* @param {Function} fn callback which will be passed a CommanderError
* @param {Function} [fn] optional callback which will be passed a CommanderError, defaults to throwing
* @return {Command} for chaining
* @api public
*/

Command.prototype._exitOverride = function(fn) {
Command.prototype.exitOverride = function(fn) {
if (fn) {
this._exitCallback = fn;
} else {
Expand Down
4 changes: 2 additions & 2 deletions tests/args.variadic.test.js
Expand Up @@ -71,7 +71,7 @@ describe('.version', () => {
test('when program variadic argument not last then error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.arguments('<variadicArg...> [optionalArg]')
.action(jest.fn);

Expand All @@ -83,7 +83,7 @@ describe('.version', () => {
test('when command variadic argument not last then error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.command('sub <variadicArg...> [optionalArg]')
.action(jest.fn);

Expand Down
8 changes: 4 additions & 4 deletions tests/command.allowUnknownOptions.test.js
Expand Up @@ -21,7 +21,7 @@ describe('.version', () => {
test('when specify unknown program option then error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.option('-p, --pepper', 'add pepper');

expect(() => {
Expand All @@ -32,7 +32,7 @@ describe('.version', () => {
test('when specify unknown program option and allowUnknownOption then no error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.allowUnknownOption()
.option('-p, --pepper', 'add pepper');

Expand All @@ -44,7 +44,7 @@ describe('.version', () => {
test('when specify unknown command option then error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.command('sub')
.option('-p, --pepper', 'add pepper')
.action(() => { });
Expand All @@ -57,7 +57,7 @@ describe('.version', () => {
test('when specify unknown command option and allowUnknownOption then no error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.command('sub')
.allowUnknownOption()
.option('-p, --pepper', 'add pepper')
Expand Down
2 changes: 1 addition & 1 deletion tests/command.executableSubcommand.test.js
Expand Up @@ -8,7 +8,7 @@ test('when no command missing then display help', () => {
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
const program = new commander.Command();
program
._exitOverride((err) => { throw err; })
.exitOverride((err) => { throw err; })
.command('install', 'install description');
expect(() => {
program.parse(['node', 'test']);
Expand Down
20 changes: 10 additions & 10 deletions tests/command.exitOverride.test.js
Expand Up @@ -36,7 +36,7 @@ describe('.exitOverride and error details', () => {
test('when specify unknown program option then throw CommanderError', () => {
const program = new commander.Command();
program
._exitOverride();
.exitOverride();

let caughtErr;
try {
Expand All @@ -54,7 +54,7 @@ describe('.exitOverride and error details', () => {
const customError = new commander.CommanderError(123, 'custom-code', 'custom-message');
const program = new commander.Command();
program
._exitOverride((_err) => {
.exitOverride((_err) => {
throw customError;
});

Expand All @@ -72,7 +72,7 @@ describe('.exitOverride and error details', () => {
const optionFlags = '-p, --pepper <type>';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.option(optionFlags, 'add pepper');

let caughtErr;
Expand All @@ -89,7 +89,7 @@ describe('.exitOverride and error details', () => {
test('when specify command without required argument then throw CommanderError', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.command('compress <arg-name>')
.action(() => { });

Expand All @@ -107,7 +107,7 @@ describe('.exitOverride and error details', () => {
test('when specify --help then throw CommanderError', () => {
const program = new commander.Command();
program
._exitOverride();
.exitOverride();

let caughtErr;
try {
Expand All @@ -123,7 +123,7 @@ describe('.exitOverride and error details', () => {
test('when executable subcommand and no command specified then throw CommanderError', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.command('compress', 'compress description');

let caughtErr;
Expand All @@ -142,7 +142,7 @@ describe('.exitOverride and error details', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.version(myVersion);

let caughtErr;
Expand All @@ -160,7 +160,7 @@ describe('.exitOverride and error details', () => {
// Note: this error is notified during parse, although could have been detected at declaration.
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.arguments('<myVariadicArg...> [optionalArg]')
.action(jest.fn);

Expand All @@ -179,7 +179,7 @@ describe('.exitOverride and error details', () => {
const pm = path.join(__dirname, 'fixtures/pm');
const program = new commander.Command();
program
._exitOverride((err) => {
.exitOverride((err) => {
expectCommanderError(err, 0, 'commander.executeSubCommandAsync', '(close)');
done();
})
Expand All @@ -202,7 +202,7 @@ describe('.exitOverride and error details', () => {
const pm = path.join(__dirname, 'fixtures/pm');
const program = new commander.Command();
program
._exitOverride(exitCallback)
.exitOverride(exitCallback)
.command('does-not-exist', 'fail');

program.parse(['node', pm, 'does-not-exist']);
Expand Down
6 changes: 3 additions & 3 deletions tests/command.help.test.js
Expand Up @@ -40,7 +40,7 @@ test('when call .help then exit', () => {
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
const program = new commander.Command();
program
._exitOverride();
.exitOverride();
expect(() => {
program.help();
}).toThrow('(outputHelp)');
Expand All @@ -52,7 +52,7 @@ test('when specify --help then exit', () => {
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
const program = new commander.Command();
program
._exitOverride();
.exitOverride();
expect(() => {
program.parse(['node', 'test', '--help']);
}).toThrow('(outputHelp)');
Expand All @@ -65,7 +65,7 @@ test('when call help(cb) then display cb output and exit', () => {
const helpReplacement = 'reformatted help';
const program = new commander.Command();
program
._exitOverride();
.exitOverride();
expect(() => {
program.help((helpInformation) => {
return helpReplacement;
Expand Down
2 changes: 1 addition & 1 deletion tests/command.helpOption.test.js
Expand Up @@ -5,7 +5,7 @@ test('when helpOption has custom flags then custom flag invokes help', () => {
const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { });
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.helpOption('--custom-help', 'custom help output');
expect(() => {
program.parse(['node', 'test', '--custom-help']);
Expand Down
6 changes: 3 additions & 3 deletions tests/openIssues.test.js.skip
Expand Up @@ -7,7 +7,7 @@ describe('open issues', () => {
test('#1039: when unknown option then unknown option detected', () => {
const program = new commander.Command();
program
._exitOverride();
.exitOverride();
expect(() => {
program.parse(['node', 'text', '--bug']);
}).toThrow();
Expand All @@ -16,7 +16,7 @@ describe('open issues', () => {
test('#1039: when unknown option and multiple arguments then unknown option detected', () => {
const program = new commander.Command();
program
._exitOverride();
.exitOverride();
expect(() => {
program.parse(['node', 'text', '--bug', '0', '1', '2', '3']);
}).toThrow();
Expand Down Expand Up @@ -49,7 +49,7 @@ describe('open issues', () => {
test('#561: when specify argument and unknown option then error', () => {
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.option('-x, --x-flag', 'A flag')
.arguments('<file>');

Expand Down
12 changes: 6 additions & 6 deletions tests/options.version.test.js
@@ -1,6 +1,6 @@
const commander = require('../');

// Test .version. Using _exitOverride to check behaviour (instead of mocking process.exit).
// Test .version. Using exitOverride to check behaviour (instead of mocking process.exit).

describe('.version', () => {
// Optional. Suppress normal output to keep test output clean.
Expand Down Expand Up @@ -42,7 +42,7 @@ describe('.version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.version(myVersion);

expect(() => {
Expand All @@ -57,7 +57,7 @@ describe('.version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.version(myVersion);

expect(() => {
Expand All @@ -81,7 +81,7 @@ describe('.version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.version(myVersion, '-r, --revision');

expect(() => {
Expand All @@ -93,7 +93,7 @@ describe('.version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.version(myVersion, '-r, --revision');

expect(() => {
Expand Down Expand Up @@ -132,7 +132,7 @@ describe('.version', () => {
const myVersion = '1.2.3';
const program = new commander.Command();
program
._exitOverride()
.exitOverride()
.version(myVersion)
.command('version')
.action(() => {});
Expand Down
19 changes: 19 additions & 0 deletions typings/commander-tests.ts
Expand Up @@ -6,6 +6,7 @@ interface ExtendedOptions extends program.CommandOptions {

const commandInstance = new program.Command('-f');
const optionsInstance = new program.Option('-f');
const errorInstance = new program.CommanderError(1, 'code', 'message');

const name = program.name();

Expand Down Expand Up @@ -99,6 +100,24 @@ program
.command("name1", "description")
.command("name2", "description", { isDefault:true })

program
.exitOverride();

program.exitOverride((err):never => {
console.log(err.code);
console.log(err.message);
console.log(err.nestedError);
return process.exit(err.exitCode);
});

program.exitOverride((err):void => {
if (err.code !== 'commander.executeSubCommandAsync') {
throw err;
} else {
// Async callback from spawn events, not useful to throw.
}
});

program.parse(process.argv);

console.log('stuff');

0 comments on commit 8df692e

Please sign in to comment.