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 showHelpAfterError #1534

Merged
merged 12 commits into from Jun 1, 2021
18 changes: 18 additions & 0 deletions Readme.md
Expand Up @@ -30,6 +30,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [Life cycle hooks](#life-cycle-hooks)
- [Automated help](#automated-help)
- [Custom help](#custom-help)
- [Display help after errors](#display-help-after-errors)
- [Display help from code](#display-help-from-code)
- [.usage and .name](#usage-and-name)
- [.helpOption(flags, description)](#helpoptionflags-description)
Expand Down Expand Up @@ -668,6 +669,23 @@ The second parameter can be a string, or a function returning a string. The func
- error: a boolean for whether the help is being displayed due to a usage error
- command: the Command which is displaying the help

### Display help after errors

The default behaviour for usage errors is to just display a short error message.
You can change the behaviour to show the full help or a custom help message after an error.

```js
program.showHelpAfterError();
// or
program.showHelpAfterError('(add --help for additional information)');
```

```sh
$ pizza --unknown
error: unknown option '--unknown'
(add --help for additional information)
```

### Display help from code

`.help()`: display help information and exit immediately. You can optionally pass `{ error: true }` to display on stderr and exit with an error status.
Expand Down
29 changes: 22 additions & 7 deletions lib/command.js
Expand Up @@ -42,6 +42,8 @@ class Command extends EventEmitter {
this._enablePositionalOptions = false;
this._passThroughOptions = false;
this._lifeCycleHooks = {}; // a hash of arrays
/** @type {boolean | string} */
this._showHelpAfterError = false;

// see .configureOutput() for docs
this._outputConfiguration = {
Expand Down Expand Up @@ -125,6 +127,7 @@ class Command extends EventEmitter {
cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue;
cmd._allowExcessArguments = this._allowExcessArguments;
cmd._enablePositionalOptions = this._enablePositionalOptions;
cmd._showHelpAfterError = this._showHelpAfterError;

cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor
if (args) cmd.arguments(args);
Expand Down Expand Up @@ -201,6 +204,18 @@ class Command extends EventEmitter {
return this;
}

/**
* Display the help or a custom message after an error occurs.
*
* @param {boolean|string} [displayHelp]
* @return {Command} `this` command for chaining
*/
showHelpAfterError(displayHelp = true) {
if (typeof displayHelp !== 'string') displayHelp = !!displayHelp;
this._showHelpAfterError = displayHelp;
return this;
}

/**
* Add a prepared subcommand.
*
Expand Down Expand Up @@ -1370,6 +1385,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/
_displayError(exitCode, code, message) {
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 });
}
this._exit(exitCode, code, message);
}

Expand Down Expand Up @@ -1446,13 +1467,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
*/

unknownCommand() {
const partCommands = [this.name()];
for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) {
partCommands.unshift(parentCmd.name());
}
const fullCommand = partCommands.join(' ');
const message = `error: unknown command '${this.args[0]}'.` +
(this._hasHelpOption ? ` See '${fullCommand} ${this._helpLongFlag}'.` : '');
const message = `error: unknown command '${this.args[0]}'`;
this._displayError(1, 'commander.unknownCommand', message);
};

Expand Down
6 changes: 6 additions & 0 deletions tests/command.chain.test.js
Expand Up @@ -177,4 +177,10 @@ describe('Command methods that should return this for chaining', () => {
const result = program.setOptionValue();
expect(result).toBe(program);
});

test('when call .showHelpAfterError() then returns this', () => {
const program = new Command();
const result = program.showHelpAfterError();
expect(result).toBe(program);
});
});
2 changes: 1 addition & 1 deletion tests/command.exitOverride.test.js
Expand Up @@ -62,7 +62,7 @@ describe('.exitOverride and error details', () => {
}

expect(stderrSpy).toHaveBeenCalled();
expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unknown command 'oops'. See 'prog --help'.");
expectCommanderError(caughtErr, 1, 'commander.unknownCommand', "error: unknown command 'oops'");
});

// Same error as above, but with custom handler.
Expand Down
2 changes: 1 addition & 1 deletion tests/command.helpOption.test.js
Expand Up @@ -128,6 +128,6 @@ describe('helpOption', () => {
.command('foo');
expect(() => {
program.parse(['UNKNOWN'], { from: 'user' });
}).toThrow("error: unknown command 'UNKNOWN'.");
}).toThrow("error: unknown command 'UNKNOWN'");
});
});
122 changes: 122 additions & 0 deletions tests/command.showHelpAfterError.test.js
@@ -0,0 +1,122 @@
const commander = require('../');

describe('showHelpAfterError with message', () => {
const customHelpMessage = 'See --help';

function makeProgram() {
const writeMock = jest.fn();
const program = new commander.Command();
program
.exitOverride()
.showHelpAfterError(customHelpMessage)
.configureOutput({ writeErr: writeMock });

return { program, writeMock };
}

test('when missing command-argument then shows help', () => {
const { program, writeMock } = makeProgram();
program.argument('<file>');
let caughtErr;
try {
program.parse([], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.missingArgument');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});

test('when missing option-argument then shows help', () => {
const { program, writeMock } = makeProgram();
program.option('--output <file>');
let caughtErr;
try {
program.parse(['--output'], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.optionMissingArgument');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});

test('when missing mandatory option then shows help', () => {
const { program, writeMock } = makeProgram();
program.requiredOption('--password <cipher>');
let caughtErr;
try {
program.parse([], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.missingMandatoryOptionValue');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});

test('when unknown option then shows help', () => {
const { program, writeMock } = makeProgram();
let caughtErr;
try {
program.parse(['--unknown-option'], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.unknownOption');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});

test('when too many command-arguments then shows help', () => {
const { program, writeMock } = makeProgram();
program
.allowExcessArguments(false);
let caughtErr;
try {
program.parse(['surprise'], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.excessArguments');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});

test('when unknown command then shows help', () => {
const { program, writeMock } = makeProgram();
program.command('sub1');
let caughtErr;
try {
program.parse(['sub2'], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.unknownCommand');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});

test('when invalid option choice then shows help', () => {
const { program, writeMock } = makeProgram();
program.addOption(new commander.Option('--color').choices(['red', 'blue']));
let caughtErr;
try {
program.parse(['--color', 'pink'], { from: 'user' });
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.invalidArgument');
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
});
});

test('when showHelpAfterError() and error and then shows full help', () => {
const writeMock = jest.fn();
const program = new commander.Command();
program
.exitOverride()
.showHelpAfterError()
.configureOutput({ writeErr: writeMock });

try {
program.parse(['--unknown-option'], { from: 'user' });
} catch (err) {
}
expect(writeMock).toHaveBeenLastCalledWith(program.helpInformation());
});
16 changes: 0 additions & 16 deletions tests/command.unknownCommand.test.js
Expand Up @@ -78,20 +78,4 @@ describe('unknownCommand', () => {
}
expect(caughtErr.code).toBe('commander.unknownCommand');
});

test('when unknown subcommand then help suggestion includes command path', () => {
const program = new commander.Command();
program
.exitOverride()
.command('sub')
.command('subsub');
let caughtErr;
try {
program.parse('node test.js sub unknown'.split(' '));
} catch (err) {
caughtErr = err;
}
expect(caughtErr.code).toBe('commander.unknownCommand');
expect(writeErrorSpy.mock.calls[0][0]).toMatch('test sub');
});
});
5 changes: 5 additions & 0 deletions typings/index.d.ts
Expand Up @@ -388,6 +388,11 @@ export class Command {
/** Get configuration */
configureOutput(): OutputConfiguration;

/**
* Display the help or a custom message after an error occurs.
*/
showHelpAfterError(displayHelp?: boolean | string): this;

/**
* Register callback `fn` for the command.
*
Expand Down
5 changes: 5 additions & 0 deletions typings/index.test-d.ts
Expand Up @@ -274,6 +274,11 @@ expectType<commander.Command>(program.configureHelp({
}));
expectType<commander.HelpConfiguration>(program.configureHelp());

// showHelpAfterError
expectType<commander.Command>(program.showHelpAfterError());
expectType<commander.Command>(program.showHelpAfterError(true));
expectType<commander.Command>(program.showHelpAfterError('See --help'));

// configureOutput
expectType<commander.Command>(program.configureOutput({ }));
expectType<commander.OutputConfiguration>(program.configureOutput());
Expand Down