Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
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');
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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