From 6b0da825e772b2bdb714f345cb305624bf8517a2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 8 Jan 2022 17:57:07 +1300 Subject: [PATCH 1/5] Refactor private _displayError() into public error() --- lib/command.js | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/command.js b/lib/command.js index ef7a98371..ac0ad4314 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; } @@ -1462,11 +1462,15 @@ Expecting one of '${allowedValues.join("', '")}'`); } /** - * Internal bottleneck for handling of parsing errors. + * Display error message and exit (or call exit override). * - * @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`); @@ -1474,6 +1478,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); } @@ -1510,7 +1519,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' }); } /** @@ -1522,7 +1531,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' }); } /** @@ -1534,7 +1543,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' }); } /** @@ -1563,7 +1572,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', message }); } /** @@ -1580,7 +1589,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' }); } /** @@ -1604,7 +1613,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' }); } /** From 63aefa64414d4eaa1d624e8d7de35800827b03d8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 11 Jan 2022 21:40:13 +1300 Subject: [PATCH 2/5] add TypeScript --- lib/command.js | 4 ++-- typings/index.d.ts | 12 ++++++++++++ typings/index.test-d.ts | 6 ++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/command.js b/lib/command.js index ac0ad4314..afe0a9e14 100644 --- a/lib/command.js +++ b/lib/command.js @@ -1462,7 +1462,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } /** - * Display error message and exit (or call exit override). + * Display error message and exit (or call exitOverride). * * @param {string} message * @param {Object} [errorOptions] @@ -1572,7 +1572,7 @@ Expecting one of '${allowedValues.join("', '")}'`); } const message = `error: unknown option '${flag}'${suggestion}`; - this.error(message, { code: 'commander.unknownOption', message }); + this.error(message, { code: 'commander.unknownOption' }); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index b2a700502..09a747964 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 09d1c085c..1fe14d737 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', () => {})); From 8911642f683621c0b0fb80f695754e4abc87e020 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 13 Jan 2022 20:55:38 +1300 Subject: [PATCH 3/5] Add tests --- tests/command.error.test.js | 57 ++++++++++++++++++++++++++++++ tests/command.exitOverride.test.js | 15 ++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/command.error.test.js 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)', () => { From be27c02e3c77a77036b6684cc1f464fc2c0edd6c Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 13 Jan 2022 21:20:58 +1300 Subject: [PATCH 4/5] Add to README --- Readme.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Readme.md b/Readme.md index 63c443c40..42fb13750 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 an 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 From 22b6b145f565c818d8d39a800b30d22b1764cb18 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 13 Jan 2022 21:24:42 +1300 Subject: [PATCH 5/5] Tiny wording change --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 42fb13750..f1b1b4c3a 100644 --- a/Readme.md +++ b/Readme.md @@ -1008,7 +1008,7 @@ If you are using VSCode to debug executable subcommands you need to set the `"au 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 an error message, you can optionally specify the `exitCode` (used with `process.exit`) +As well as the error message, you can optionally specify the `exitCode` (used with `process.exit`) and `code` (used with `CommanderError`). ```js