diff --git a/Readme.md b/Readme.md index 63c443c40..f1b1b4c3a 100644 --- a/Readme.md +++ b/Readme.md @@ -47,6 +47,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [createCommand()](#createcommand) - [Node options such as `--harmony`](#node-options-such-as---harmony) - [Debugging stand-alone executable subcommands](#debugging-stand-alone-executable-subcommands) + - [Display error](#display-error) - [Override exit and output handling](#override-exit-and-output-handling) - [Additional documentation](#additional-documentation) - [Support](#support) @@ -1003,6 +1004,18 @@ the inspector port is incremented by 1 for the spawned subcommand. If you are using VSCode to debug executable subcommands you need to set the `"autoAttachChildProcesses": true` flag in your launch.json configuration. +### Display error + +This routine is available to invoke the Commander error handling for your own error conditions. (See also the next section about exit handling.) + +As well as the error message, you can optionally specify the `exitCode` (used with `process.exit`) +and `code` (used with `CommanderError`). + +```js +program.exit('Password must be longer than four characters'); +program.exit('Custom processing has failed', { exitCode: 2, code: 'my.custom.error' }); +``` + ### Override exit and output handling By default Commander calls `process.exit` when it detects errors, or after displaying the help or version. You can override diff --git a/lib/command.js b/lib/command.js index ffd8a1d46..aa0d7c03d 100644 --- a/lib/command.js +++ b/lib/command.js @@ -538,7 +538,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } catch (err) { if (err.code === 'commander.invalidArgument') { const message = `${invalidValueMessage} ${err.message}`; - this._displayError(err.exitCode, err.code, message); + this.error(message, { exitCode: err.exitCode, code: err.code }); } throw err; } @@ -1096,7 +1096,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } catch (err) { if (err.code === 'commander.invalidArgument') { const message = `error: command-argument value '${value}' is invalid for argument '${argument.name()}'. ${err.message}`; - this._displayError(err.exitCode, err.code, message); + this.error(message, { exitCode: err.exitCode, code: err.code }); } throw err; } @@ -1475,11 +1475,15 @@ Expecting one of '${allowedValues.join("', '")}'`); } /** - * Internal bottleneck for handling of parsing errors. + * Display error message and exit (or call exitOverride). * - * @api private + * @param {string} message + * @param {Object} [errorOptions] + * @param {string} [errorOptions.code] - an id string representing the error + * @param {number} [errorOptions.exitCode] - used with process.exit */ - _displayError(exitCode, code, message) { + error(message, errorOptions) { + // output handling this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr); if (typeof this._showHelpAfterError === 'string') { this._outputConfiguration.writeErr(`${this._showHelpAfterError}\n`); @@ -1487,6 +1491,11 @@ Expecting one of '${allowedValues.join("', '")}'`); this._outputConfiguration.writeErr('\n'); this.outputHelp({ error: true }); } + + // exit handling + const config = errorOptions || {}; + const exitCode = config.exitCode || 1; + const code = config.code || 'commander.error'; this._exit(exitCode, code, message); } @@ -1523,7 +1532,7 @@ Expecting one of '${allowedValues.join("', '")}'`); missingArgument(name) { const message = `error: missing required argument '${name}'`; - this._displayError(1, 'commander.missingArgument', message); + this.error(message, { code: 'commander.missingArgument' }); } /** @@ -1535,7 +1544,7 @@ Expecting one of '${allowedValues.join("', '")}'`); optionMissingArgument(option) { const message = `error: option '${option.flags}' argument missing`; - this._displayError(1, 'commander.optionMissingArgument', message); + this.error(message, { code: 'commander.optionMissingArgument' }); } /** @@ -1547,7 +1556,7 @@ Expecting one of '${allowedValues.join("', '")}'`); missingMandatoryOptionValue(option) { const message = `error: required option '${option.flags}' not specified`; - this._displayError(1, 'commander.missingMandatoryOptionValue', message); + this.error(message, { code: 'commander.missingMandatoryOptionValue' }); } /** @@ -1576,7 +1585,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } const message = `error: unknown option '${flag}'${suggestion}`; - this._displayError(1, 'commander.unknownOption', message); + this.error(message, { code: 'commander.unknownOption' }); } /** @@ -1593,7 +1602,7 @@ Expecting one of '${allowedValues.join("', '")}'`); const s = (expected === 1) ? '' : 's'; const forSubcommand = this.parent ? ` for '${this.name()}'` : ''; const message = `error: too many arguments${forSubcommand}. Expected ${expected} argument${s} but got ${receivedArgs.length}.`; - this._displayError(1, 'commander.excessArguments', message); + this.error(message, { code: 'commander.excessArguments' }); } /** @@ -1617,7 +1626,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } const message = `error: unknown command '${unknownName}'${suggestion}`; - this._displayError(1, 'commander.unknownCommand', message); + this.error(message, { code: 'commander.unknownCommand' }); } /** diff --git a/tests/command.error.test.js b/tests/command.error.test.js new file mode 100644 index 000000000..640670124 --- /dev/null +++ b/tests/command.error.test.js @@ -0,0 +1,57 @@ +const commander = require('../'); + +test('when error called with message then message displayed on stderr', () => { + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { }); + const stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => { }); + + const program = new commander.Command(); + const message = 'Goodbye'; + program.error(message); + + expect(stderrSpy).toHaveBeenCalledWith(`${message}\n`); + stderrSpy.mockRestore(); + exitSpy.mockRestore(); +}); + +test('when error called with no exitCode then process.exit(1)', () => { + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { }); + + const program = new commander.Command(); + program.configureOutput({ + writeErr: () => {} + }); + + program.error('Goodbye'); + + expect(exitSpy).toHaveBeenCalledWith(1); + exitSpy.mockRestore(); +}); + +test('when error called with exitCode 2 then process.exit(2)', () => { + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => { }); + + const program = new commander.Command(); + program.configureOutput({ + writeErr: () => {} + }); + program.error('Goodbye', { exitCode: 2 }); + + expect(exitSpy).toHaveBeenCalledWith(2); + exitSpy.mockRestore(); +}); + +test('when error called with code and exitOverride then throws with code', () => { + const program = new commander.Command(); + let errorThrown; + program + .exitOverride((err) => { errorThrown = err; throw err; }) + .configureOutput({ + writeErr: () => {} + }); + + const code = 'commander.test'; + expect(() => { + program.error('Goodbye', { code }); + }).toThrow(); + expect(errorThrown.code).toEqual(code); +}); diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index bdd7fe7c9..09b7b4989 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -331,6 +331,21 @@ describe('.exitOverride and error details', () => { expectCommanderError(caughtErr, 1, 'commander.invalidArgument', "error: command-argument value 'green' is invalid for argument 'n'. NO"); }); + + test('when call error() then throw CommanderError', () => { + const program = new commander.Command(); + program + .exitOverride(); + + let caughtErr; + try { + program.error('message'); + } catch (err) { + caughtErr = err; + } + + expectCommanderError(caughtErr, 1, 'commander.error', 'message'); + }); }); test('when no override and error then exit(1)', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index f47fddee3..cb7bf608b 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -31,6 +31,13 @@ export class InvalidArgumentError extends CommanderError { } export { InvalidArgumentError as InvalidOptionArgumentError }; // deprecated old name +export interface ErrorOptions { // optional parameter for error() + /** an id string representing the error */ + code?: string; + /** suggested exit code which could be used with process.exit */ + exitCode?: number; +} + export class Argument { description: string; required: boolean; @@ -387,6 +394,11 @@ export class Command { */ exitOverride(callback?: (err: CommanderError) => never|void): this; + /** + * Display error message and exit (or call exitOverride). + */ + error(message: string, errorOptions?: ErrorOptions): never; + /** * You can customise the help with a subclass of Help by overriding createHelp, * or by overriding Help properties using configureHelp(). diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 858c8f0bd..a5536e542 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -75,6 +75,12 @@ expectType(program.exitOverride((err): void => { } })); +// error +expectType(program.error('Goodbye')); +expectType(program.error('Goodbye', { code: 'my.error' })); +expectType(program.error('Goodbye', { exitCode: 2 })); +expectType(program.error('Goodbye', { code: 'my.error', exitCode: 2 })); + // hook expectType(program.hook('preAction', () => {})); expectType(program.hook('postAction', () => {}));