Skip to content

Commit

Permalink
Add showHelpAfterError (#1534)
Browse files Browse the repository at this point in the history
* First cut at displayHelpWithError

* Rename method

* Rename

* Add typings

* Simplify unknownCommand message to match others

* Add chain test

* Add JSDoc typing for TypeScript --checkJS

* Inherit behaviour to subcommands

* Add tests

* Add README

* Tweak wording

* Add test for invalid argument showing help
  • Loading branch information
shadowspawn committed Jun 1, 2021
1 parent 5ddc41b commit 082717f
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 25 deletions.
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 @@ -283,6 +283,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

0 comments on commit 082717f

Please sign in to comment.