Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error() for displaying errors from client code #1675

Merged
merged 6 commits into from Jan 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions Readme.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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');
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
31 changes: 20 additions & 11 deletions lib/command.js
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1475,18 +1475,27 @@ 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`);
} else if (this._showHelpAfterError) {
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);
}

Expand Down Expand Up @@ -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' });
}

/**
Expand All @@ -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' });
}

/**
Expand All @@ -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' });
}

/**
Expand Down Expand Up @@ -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' });
}

/**
Expand All @@ -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' });
}

/**
Expand All @@ -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' });
}

/**
Expand Down
57 changes: 57 additions & 0 deletions 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);
});
15 changes: 15 additions & 0 deletions tests/command.exitOverride.test.js
Expand Up @@ -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)', () => {
Expand Down
12 changes: 12 additions & 0 deletions typings/index.d.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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().
Expand Down
6 changes: 6 additions & 0 deletions typings/index.test-d.ts
Expand Up @@ -75,6 +75,12 @@ expectType<commander.Command>(program.exitOverride((err): void => {
}
}));

// error
expectType<never>(program.error('Goodbye'));
expectType<never>(program.error('Goodbye', { code: 'my.error' }));
expectType<never>(program.error('Goodbye', { exitCode: 2 }));
expectType<never>(program.error('Goodbye', { code: 'my.error', exitCode: 2 }));

// hook
expectType<commander.Command>(program.hook('preAction', () => {}));
expectType<commander.Command>(program.hook('postAction', () => {}));
Expand Down