Skip to content

Commit 082717f

Browse files
authored
Add showHelpAfterError (#1534)
* 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
1 parent 5ddc41b commit 082717f

9 files changed

+180
-25
lines changed

Readme.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
3030
- [Life cycle hooks](#life-cycle-hooks)
3131
- [Automated help](#automated-help)
3232
- [Custom help](#custom-help)
33+
- [Display help after errors](#display-help-after-errors)
3334
- [Display help from code](#display-help-from-code)
3435
- [.usage and .name](#usage-and-name)
3536
- [.helpOption(flags, description)](#helpoptionflags-description)
@@ -668,6 +669,23 @@ The second parameter can be a string, or a function returning a string. The func
668669
- error: a boolean for whether the help is being displayed due to a usage error
669670
- command: the Command which is displaying the help
670671
672+
### Display help after errors
673+
674+
The default behaviour for usage errors is to just display a short error message.
675+
You can change the behaviour to show the full help or a custom help message after an error.
676+
677+
```js
678+
program.showHelpAfterError();
679+
// or
680+
program.showHelpAfterError('(add --help for additional information)');
681+
```
682+
683+
```sh
684+
$ pizza --unknown
685+
error: unknown option '--unknown'
686+
(add --help for additional information)
687+
```
688+
671689
### Display help from code
672690
673691
`.help()`: display help information and exit immediately. You can optionally pass `{ error: true }` to display on stderr and exit with an error status.

lib/command.js

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class Command extends EventEmitter {
4242
this._enablePositionalOptions = false;
4343
this._passThroughOptions = false;
4444
this._lifeCycleHooks = {}; // a hash of arrays
45+
/** @type {boolean | string} */
46+
this._showHelpAfterError = false;
4547

4648
// see .configureOutput() for docs
4749
this._outputConfiguration = {
@@ -125,6 +127,7 @@ class Command extends EventEmitter {
125127
cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue;
126128
cmd._allowExcessArguments = this._allowExcessArguments;
127129
cmd._enablePositionalOptions = this._enablePositionalOptions;
130+
cmd._showHelpAfterError = this._showHelpAfterError;
128131

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

207+
/**
208+
* Display the help or a custom message after an error occurs.
209+
*
210+
* @param {boolean|string} [displayHelp]
211+
* @return {Command} `this` command for chaining
212+
*/
213+
showHelpAfterError(displayHelp = true) {
214+
if (typeof displayHelp !== 'string') displayHelp = !!displayHelp;
215+
this._showHelpAfterError = displayHelp;
216+
return this;
217+
}
218+
204219
/**
205220
* Add a prepared subcommand.
206221
*
@@ -1370,6 +1385,12 @@ Expecting one of '${allowedValues.join("', '")}'`);
13701385
*/
13711386
_displayError(exitCode, code, message) {
13721387
this._outputConfiguration.outputError(`${message}\n`, this._outputConfiguration.writeErr);
1388+
if (typeof this._showHelpAfterError === 'string') {
1389+
this._outputConfiguration.writeErr(`${this._showHelpAfterError}\n`);
1390+
} else if (this._showHelpAfterError) {
1391+
this._outputConfiguration.writeErr('\n');
1392+
this.outputHelp({ error: true });
1393+
}
13731394
this._exit(exitCode, code, message);
13741395
}
13751396

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

14481469
unknownCommand() {
1449-
const partCommands = [this.name()];
1450-
for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) {
1451-
partCommands.unshift(parentCmd.name());
1452-
}
1453-
const fullCommand = partCommands.join(' ');
1454-
const message = `error: unknown command '${this.args[0]}'.` +
1455-
(this._hasHelpOption ? ` See '${fullCommand} ${this._helpLongFlag}'.` : '');
1470+
const message = `error: unknown command '${this.args[0]}'`;
14561471
this._displayError(1, 'commander.unknownCommand', message);
14571472
};
14581473

tests/command.chain.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,10 @@ describe('Command methods that should return this for chaining', () => {
177177
const result = program.setOptionValue();
178178
expect(result).toBe(program);
179179
});
180+
181+
test('when call .showHelpAfterError() then returns this', () => {
182+
const program = new Command();
183+
const result = program.showHelpAfterError();
184+
expect(result).toBe(program);
185+
});
180186
});

tests/command.exitOverride.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('.exitOverride and error details', () => {
6262
}
6363

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

6868
// Same error as above, but with custom handler.

tests/command.helpOption.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,6 @@ describe('helpOption', () => {
128128
.command('foo');
129129
expect(() => {
130130
program.parse(['UNKNOWN'], { from: 'user' });
131-
}).toThrow("error: unknown command 'UNKNOWN'.");
131+
}).toThrow("error: unknown command 'UNKNOWN'");
132132
});
133133
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const commander = require('../');
2+
3+
describe('showHelpAfterError with message', () => {
4+
const customHelpMessage = 'See --help';
5+
6+
function makeProgram() {
7+
const writeMock = jest.fn();
8+
const program = new commander.Command();
9+
program
10+
.exitOverride()
11+
.showHelpAfterError(customHelpMessage)
12+
.configureOutput({ writeErr: writeMock });
13+
14+
return { program, writeMock };
15+
}
16+
17+
test('when missing command-argument then shows help', () => {
18+
const { program, writeMock } = makeProgram();
19+
program.argument('<file>');
20+
let caughtErr;
21+
try {
22+
program.parse([], { from: 'user' });
23+
} catch (err) {
24+
caughtErr = err;
25+
}
26+
expect(caughtErr.code).toBe('commander.missingArgument');
27+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
28+
});
29+
30+
test('when missing option-argument then shows help', () => {
31+
const { program, writeMock } = makeProgram();
32+
program.option('--output <file>');
33+
let caughtErr;
34+
try {
35+
program.parse(['--output'], { from: 'user' });
36+
} catch (err) {
37+
caughtErr = err;
38+
}
39+
expect(caughtErr.code).toBe('commander.optionMissingArgument');
40+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
41+
});
42+
43+
test('when missing mandatory option then shows help', () => {
44+
const { program, writeMock } = makeProgram();
45+
program.requiredOption('--password <cipher>');
46+
let caughtErr;
47+
try {
48+
program.parse([], { from: 'user' });
49+
} catch (err) {
50+
caughtErr = err;
51+
}
52+
expect(caughtErr.code).toBe('commander.missingMandatoryOptionValue');
53+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
54+
});
55+
56+
test('when unknown option then shows help', () => {
57+
const { program, writeMock } = makeProgram();
58+
let caughtErr;
59+
try {
60+
program.parse(['--unknown-option'], { from: 'user' });
61+
} catch (err) {
62+
caughtErr = err;
63+
}
64+
expect(caughtErr.code).toBe('commander.unknownOption');
65+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
66+
});
67+
68+
test('when too many command-arguments then shows help', () => {
69+
const { program, writeMock } = makeProgram();
70+
program
71+
.allowExcessArguments(false);
72+
let caughtErr;
73+
try {
74+
program.parse(['surprise'], { from: 'user' });
75+
} catch (err) {
76+
caughtErr = err;
77+
}
78+
expect(caughtErr.code).toBe('commander.excessArguments');
79+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
80+
});
81+
82+
test('when unknown command then shows help', () => {
83+
const { program, writeMock } = makeProgram();
84+
program.command('sub1');
85+
let caughtErr;
86+
try {
87+
program.parse(['sub2'], { from: 'user' });
88+
} catch (err) {
89+
caughtErr = err;
90+
}
91+
expect(caughtErr.code).toBe('commander.unknownCommand');
92+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
93+
});
94+
95+
test('when invalid option choice then shows help', () => {
96+
const { program, writeMock } = makeProgram();
97+
program.addOption(new commander.Option('--color').choices(['red', 'blue']));
98+
let caughtErr;
99+
try {
100+
program.parse(['--color', 'pink'], { from: 'user' });
101+
} catch (err) {
102+
caughtErr = err;
103+
}
104+
expect(caughtErr.code).toBe('commander.invalidArgument');
105+
expect(writeMock).toHaveBeenLastCalledWith(`${customHelpMessage}\n`);
106+
});
107+
});
108+
109+
test('when showHelpAfterError() and error and then shows full help', () => {
110+
const writeMock = jest.fn();
111+
const program = new commander.Command();
112+
program
113+
.exitOverride()
114+
.showHelpAfterError()
115+
.configureOutput({ writeErr: writeMock });
116+
117+
try {
118+
program.parse(['--unknown-option'], { from: 'user' });
119+
} catch (err) {
120+
}
121+
expect(writeMock).toHaveBeenLastCalledWith(program.helpInformation());
122+
});

tests/command.unknownCommand.test.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,4 @@ describe('unknownCommand', () => {
7878
}
7979
expect(caughtErr.code).toBe('commander.unknownCommand');
8080
});
81-
82-
test('when unknown subcommand then help suggestion includes command path', () => {
83-
const program = new commander.Command();
84-
program
85-
.exitOverride()
86-
.command('sub')
87-
.command('subsub');
88-
let caughtErr;
89-
try {
90-
program.parse('node test.js sub unknown'.split(' '));
91-
} catch (err) {
92-
caughtErr = err;
93-
}
94-
expect(caughtErr.code).toBe('commander.unknownCommand');
95-
expect(writeErrorSpy.mock.calls[0][0]).toMatch('test sub');
96-
});
9781
});

typings/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,11 @@ export class Command {
388388
/** Get configuration */
389389
configureOutput(): OutputConfiguration;
390390

391+
/**
392+
* Display the help or a custom message after an error occurs.
393+
*/
394+
showHelpAfterError(displayHelp?: boolean | string): this;
395+
391396
/**
392397
* Register callback `fn` for the command.
393398
*

typings/index.test-d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,11 @@ expectType<commander.Command>(program.configureHelp({
283283
}));
284284
expectType<commander.HelpConfiguration>(program.configureHelp());
285285

286+
// showHelpAfterError
287+
expectType<commander.Command>(program.showHelpAfterError());
288+
expectType<commander.Command>(program.showHelpAfterError(true));
289+
expectType<commander.Command>(program.showHelpAfterError('See --help'));
290+
286291
// configureOutput
287292
expectType<commander.Command>(program.configureOutput({ }));
288293
expectType<commander.OutputConfiguration>(program.configureOutput());

0 commit comments

Comments
 (0)