diff --git a/Readme.md b/Readme.md index 7f180c733..9404ce627 100644 --- a/Readme.md +++ b/Readme.md @@ -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) @@ -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 diff --git a/index.js b/index.js index 70c2d1494..ad5d82f4a 100644 --- a/index.js +++ b/index.js @@ -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 { diff --git a/tests/args.variadic.test.js b/tests/args.variadic.test.js index cf011381c..97c8d8f4a 100644 --- a/tests/args.variadic.test.js +++ b/tests/args.variadic.test.js @@ -71,7 +71,7 @@ describe('.version', () => { test('when program variadic argument not last then error', () => { const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .arguments(' [optionalArg]') .action(jest.fn); @@ -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 [optionalArg]') .action(jest.fn); diff --git a/tests/command.allowUnknownOptions.test.js b/tests/command.allowUnknownOptions.test.js index d6353d4bb..f86ae9f75 100644 --- a/tests/command.allowUnknownOptions.test.js +++ b/tests/command.allowUnknownOptions.test.js @@ -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(() => { @@ -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'); @@ -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(() => { }); @@ -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') diff --git a/tests/command.executableSubcommand.test.js b/tests/command.executableSubcommand.test.js index f97529956..8c6076cb0 100644 --- a/tests/command.executableSubcommand.test.js +++ b/tests/command.executableSubcommand.test.js @@ -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']); diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index 0df6451e9..a69366bda 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -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 { @@ -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; }); @@ -72,7 +72,7 @@ describe('.exitOverride and error details', () => { const optionFlags = '-p, --pepper '; const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .option(optionFlags, 'add pepper'); let caughtErr; @@ -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 ') .action(() => { }); @@ -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 { @@ -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; @@ -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; @@ -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(' [optionalArg]') .action(jest.fn); @@ -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(); }) @@ -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']); diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 6cc7d412f..b61fbc016 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -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)'); @@ -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)'); @@ -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; diff --git a/tests/command.helpOption.test.js b/tests/command.helpOption.test.js index b99eaaec2..f8b96b677 100644 --- a/tests/command.helpOption.test.js +++ b/tests/command.helpOption.test.js @@ -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']); diff --git a/tests/openIssues.test.js.skip b/tests/openIssues.test.js.skip index 957ae142f..78c4dd14b 100644 --- a/tests/openIssues.test.js.skip +++ b/tests/openIssues.test.js.skip @@ -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(); @@ -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(); @@ -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(''); diff --git a/tests/options.version.test.js b/tests/options.version.test.js index f0f92bdf2..d636bfdf4 100644 --- a/tests/options.version.test.js +++ b/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. @@ -42,7 +42,7 @@ describe('.version', () => { const myVersion = '1.2.3'; const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .version(myVersion); expect(() => { @@ -57,7 +57,7 @@ describe('.version', () => { const myVersion = '1.2.3'; const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .version(myVersion); expect(() => { @@ -81,7 +81,7 @@ describe('.version', () => { const myVersion = '1.2.3'; const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .version(myVersion, '-r, --revision'); expect(() => { @@ -93,7 +93,7 @@ describe('.version', () => { const myVersion = '1.2.3'; const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .version(myVersion, '-r, --revision'); expect(() => { @@ -132,7 +132,7 @@ describe('.version', () => { const myVersion = '1.2.3'; const program = new commander.Command(); program - ._exitOverride() + .exitOverride() .version(myVersion) .command('version') .action(() => {}); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 94353c40e..9b6f55147 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -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(); @@ -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'); diff --git a/typings/index.d.ts b/typings/index.d.ts index b2fd9880e..2d94eb14e 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -7,6 +7,15 @@ declare namespace local { + class CommanderError extends Error { + code: string; + exitCode: number; + message: string; + nestedError?: string; + + constructor(exitCode: number, code: string, message: string); + } + class Option { flags: string; required: boolean; @@ -26,7 +35,7 @@ declare namespace local { } class Command extends NodeJS.EventEmitter { - [key: string]: any; + [key: string]: any; // options as properties args: string[]; @@ -98,14 +107,19 @@ declare namespace local { arguments(desc: string): Command; /** - * Parse expected `args`. + * Parse expected `args`. * * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. * * @param {string[]} args * @returns {Command} for chaining */ - parseExpectedArgs(args: string[]): Command; + parseExpectedArgs(args: string[]): Command; + + /** + * Register callback to use as replacement for calling process.exit. + */ + exitOverride(callback?: (err: CommanderError) => never|void): Command; /** * Register callback `fn` for the command. @@ -292,7 +306,8 @@ declare namespace commander { Option: typeof local.Option; CommandOptions: CommandOptions; ParseOptionsResult: ParseOptionsResult; - } + CommanderError : typeof local.CommanderError; + } }