Skip to content

Commit 4f78587

Browse files
authored
Skip unknown options check if there is a better error to display (#1464)
* Check for uknown options in multiple places, to allow skipping check * Missing command shows help, does not check for options * Check unknown commands before unknown options * Add comments * Restore legacy command:* handling * Add more tests for legacy command('*') * Add comment explaining history for test * Improve comment * Test for command suggestion despite unknown option * Improve comments and suppress test output * Add test for competing unknown command and option * Test that unknown option is an error in simple program * Fix test and comment
1 parent b040db4 commit 4f78587

File tree

4 files changed

+97
-5
lines changed

4 files changed

+97
-5
lines changed

index.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -1475,12 +1475,17 @@ class Command extends EventEmitter {
14751475

14761476
outputHelpIfRequested(this, parsed.unknown);
14771477
this._checkForMissingMandatoryOptions();
1478-
if (parsed.unknown.length > 0) {
1479-
this.unknownOption(parsed.unknown[0]);
1480-
}
1478+
1479+
// We do not always call this check to avoid masking a "better" error, like unknown command.
1480+
const checkForUnknownOptions = () => {
1481+
if (parsed.unknown.length > 0) {
1482+
this.unknownOption(parsed.unknown[0]);
1483+
}
1484+
};
14811485

14821486
const commandEvent = `command:${this.name()}`;
14831487
if (this._actionHandler) {
1488+
checkForUnknownOptions();
14841489
// Check expected arguments and collect variadic together.
14851490
const args = this.args.slice();
14861491
this._args.forEach((arg, i) => {
@@ -1498,19 +1503,24 @@ class Command extends EventEmitter {
14981503
this._actionHandler(args);
14991504
if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy
15001505
} else if (this.parent && this.parent.listenerCount(commandEvent)) {
1506+
checkForUnknownOptions();
15011507
this.parent.emit(commandEvent, operands, unknown); // legacy
15021508
} else if (operands.length) {
1503-
if (this._findCommand('*')) { // legacy
1509+
if (this._findCommand('*')) { // legacy default command
15041510
this._dispatchSubcommand('*', operands, unknown);
15051511
} else if (this.listenerCount('command:*')) {
1512+
// skip option check, emit event for possible misspelling suggestion
15061513
this.emit('command:*', operands, unknown);
15071514
} else if (this.commands.length) {
15081515
this.unknownCommand();
1516+
} else {
1517+
checkForUnknownOptions();
15091518
}
15101519
} else if (this.commands.length) {
15111520
// This command has subcommands and nothing hooked up at this level, so display help.
15121521
this.help({ error: true });
15131522
} else {
1523+
checkForUnknownOptions();
15141524
// fall through for caller to handle after calling .parse()
15151525
}
15161526
}

tests/command.asterisk.test.js

+53
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,45 @@ describe(".command('*')", () => {
6464
program.parse(['node', 'test', 'unrecognised-command']);
6565
expect(mockAction).toHaveBeenCalled();
6666
});
67+
68+
test('when unrecognised argument and known option then asterisk action called', () => {
69+
// This tests for a regression between v4 and v5. Known default option should not be rejected by program.
70+
const mockAction = jest.fn();
71+
const program = new commander.Command();
72+
program
73+
.command('install');
74+
const star = program
75+
.command('*')
76+
.arguments('[args...]')
77+
.option('-d, --debug')
78+
.action(mockAction);
79+
program.parse(['node', 'test', 'unrecognised-command', '--debug']);
80+
expect(mockAction).toHaveBeenCalled();
81+
expect(star.opts().debug).toEqual(true);
82+
});
83+
84+
test('when non-command argument and unknown option then error for unknown option', () => {
85+
// This is a change in behaviour from v2 which did not error, but is consistent with modern better detection of invalid options
86+
const mockAction = jest.fn();
87+
const program = new commander.Command();
88+
program
89+
.exitOverride()
90+
.configureOutput({
91+
writeErr: () => {}
92+
})
93+
.command('install');
94+
program
95+
.command('*')
96+
.arguments('[args...]')
97+
.action(mockAction);
98+
let caughtErr;
99+
try {
100+
program.parse(['node', 'test', 'some-argument', '--unknown']);
101+
} catch (err) {
102+
caughtErr = err;
103+
}
104+
expect(caughtErr.code).toEqual('commander.unknownOption');
105+
});
67106
});
68107

69108
// Test .on explicitly rather than assuming covered by .command
@@ -108,4 +147,18 @@ describe(".on('command:*')", () => {
108147
program.parse(['node', 'test', 'unrecognised-command']);
109148
expect(mockAction).toHaveBeenCalled();
110149
});
150+
151+
test('when unrecognised command/argument and unknown option then listener called', () => {
152+
// Give listener a chance to make a suggestion for misspelled command. The option
153+
// could only be unknown because the command is not correct.
154+
// Regression identified in https://github.com/tj/commander.js/issues/1460#issuecomment-772313494
155+
const mockAction = jest.fn();
156+
const program = new commander.Command();
157+
program
158+
.command('install');
159+
program
160+
.on('command:*', mockAction);
161+
program.parse(['node', 'test', 'intsall', '--unknown']);
162+
expect(mockAction).toHaveBeenCalled();
163+
});
111164
});

tests/command.unknownCommand.test.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const commander = require('../');
22

3-
describe('unknownOption', () => {
3+
describe('unknownCommand', () => {
44
// Optional. Use internal knowledge to suppress output to keep test output clean.
55
let writeErrorSpy;
66

@@ -63,6 +63,22 @@ describe('unknownOption', () => {
6363
expect(caughtErr.code).toBe('commander.unknownCommand');
6464
});
6565

66+
test('when unknown command and unknown option then error is for unknown command', () => {
67+
// The unknown command is more useful since the option is for an unknown command (and might be
68+
// ok if the command had been correctly spelled, say).
69+
const program = new commander.Command();
70+
program
71+
.exitOverride()
72+
.command('sub');
73+
let caughtErr;
74+
try {
75+
program.parse('node test.js sbu --silly'.split(' '));
76+
} catch (err) {
77+
caughtErr = err;
78+
}
79+
expect(caughtErr.code).toBe('commander.unknownCommand');
80+
});
81+
6682
test('when unknown subcommand then help suggestion includes command path', () => {
6783
const program = new commander.Command();
6884
program

tests/command.unknownOption.test.js

+13
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,17 @@ describe('unknownOption', () => {
8282
}
8383
expect(caughtErr.code).toBe('commander.unknownOption');
8484
});
85+
86+
test('when specify unknown option with simple program then error', () => {
87+
const program = new commander.Command();
88+
program
89+
.exitOverride();
90+
let caughtErr;
91+
try {
92+
program.parse(['node', 'test', '--NONSENSE']);
93+
} catch (err) {
94+
caughtErr = err;
95+
}
96+
expect(caughtErr.code).toBe('commander.unknownOption');
97+
});
8598
});

0 commit comments

Comments
 (0)