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

Refactor help option and improve option parsing from parent when default command is invoked implicitly #1934

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1bb30d7
Support custom flags from Option subclass in helpOption()
aweebit Aug 2, 2023
e8bea4a
Fix version() parameter type
aweebit Aug 3, 2023
e4d00db
Change initial variable values in test for better error messages
aweebit Aug 2, 2023
9857b1e
Store help option instance instead of just flags
aweebit Aug 3, 2023
da1e153
Add missing _hasHelpOption check in copyInheritedSettings test
aweebit Aug 3, 2023
35e9571
Do not use undefined long help option flag in legacy code
aweebit Aug 3, 2023
5128d5f
Merge branch 'feature/fixes' into feature/createOption-in-helpOption
aweebit Aug 3, 2023
5b93a70
Refactor help option
aweebit Aug 3, 2023
a12f5be
Run help from default command's parent only if help option is first
aweebit Aug 4, 2023
ae0291b
Fix comment placement in code
aweebit Aug 4, 2023
97f3069
Check for undefined in Option.is()
aweebit Aug 4, 2023
261a9d6
Merge branch 'feature/createOption-in-helpOption' into feature/help-o…
aweebit Aug 4, 2023
7335a9c
Add missing obscured help flag tests
aweebit Aug 4, 2023
74c1d4e
Merge branch 'feature/createOption-in-helpOption' into feature/help-o…
aweebit Aug 4, 2023
f511028
Handle obscured help flags and help for default command correctly
aweebit Aug 4, 2023
87ed7ea
Refactor parseOptions() to a good library function
aweebit Aug 4, 2023
02fde0e
Improve comment in parseOptions()
aweebit Aug 4, 2023
eae6c5f
Make passThroughOptions continue processing at unknown options
aweebit Aug 4, 2023
de710a5
Revert "Make passThroughOptions continue processing at unknown options"
aweebit Aug 5, 2023
31c03c7
Handle help option specially in parseOptions()
aweebit Aug 5, 2023
aaf23bb
Improve default command handling in parseOptions()
aweebit Aug 5, 2023
d1fd350
Refactor parseOptions() subcommand handling
aweebit Aug 5, 2023
b11e940
Improve comment on passThroughOptions
aweebit Aug 5, 2023
58b3003
Refactor if statements
aweebit Aug 5, 2023
04be1b5
Amend JSDoc for displayHelp property of parseOptions() return value
aweebit Aug 5, 2023
0bab7e7
Fix mixed up option -> command in JSDoc comment
aweebit Aug 5, 2023
cd8ded5
Improve variable name & comment in parseOptions()
aweebit Aug 5, 2023
cac1f8a
Rework comment on passThroughOptions
aweebit Aug 5, 2023
e7c790a
Fix implicit default command starting with unknown option
aweebit Aug 6, 2023
dd2c9c8
Refactor parseOptions()
aweebit Aug 6, 2023
f8e5faf
Refactor parseOptions()
aweebit Aug 11, 2023
a253ec6
Revert "Fix version() parameter type"
aweebit Aug 12, 2023
8dd417f
Fix help for commands with executable handler & only a short help flag
aweebit Aug 13, 2023
513a4b5
Merge branch 'feature/fixes' into feature/createOption-in-helpOption
aweebit Aug 13, 2023
e01bb9a
Merge branch 'feature/createOption-in-helpOption' into feature/help-o…
aweebit Aug 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
279 changes: 153 additions & 126 deletions lib/command.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ class Help {
visibleOptions(cmd) {
const visibleOptions = cmd.options.filter((option) => !option.hidden);
// Implicit help
const showShortHelpFlag = cmd._hasHelpOption && cmd._helpShortFlag && !cmd._findOption(cmd._helpShortFlag);
const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpLongFlag);
const showShortHelpFlag = cmd._hasHelpOption && cmd._helpOption.short && !cmd._findOption(cmd._helpOption.short);
const showLongHelpFlag = cmd._hasHelpOption && !cmd._findOption(cmd._helpOption.long);
if (showShortHelpFlag || showLongHelpFlag) {
let helpOption;
if (!showShortHelpFlag) {
helpOption = cmd.createOption(cmd._helpLongFlag, cmd._helpDescription);
helpOption = cmd.createOption(cmd._helpOption.long, cmd._helpDescription);
} else if (!showLongHelpFlag) {
helpOption = cmd.createOption(cmd._helpShortFlag, cmd._helpDescription);
helpOption = cmd.createOption(cmd._helpOption.short, cmd._helpDescription);
} else {
helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ class Option {
/**
* Check if `arg` matches the short or long flag.
*
* @param {string} arg
* @param {string | undefined} arg
* @return {boolean}
* @api private
*/

is(arg) {
return this.short === arg || this.long === arg;
return arg !== undefined && (this.short === arg || this.long === arg);
}

/**
Expand Down
10 changes: 5 additions & 5 deletions tests/command.addHelpText.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ describe('context checks with full parse', () => {
});

test('when help requested then context.error is false', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -193,7 +193,7 @@ describe('context checks with full parse', () => {
});

test('when help for error then context.error is true', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -206,7 +206,7 @@ describe('context checks with full parse', () => {
});

test('when help on program then context.command is program', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -218,7 +218,7 @@ describe('context checks with full parse', () => {
});

test('when help on subcommand and "before" subcommand then context.command is subcommand', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride();
Expand All @@ -231,7 +231,7 @@ describe('context checks with full parse', () => {
});

test('when help on subcommand and "beforeAll" on program then context.command is subcommand', () => {
let context = {};
let context;
const program = new commander.Command();
program
.exitOverride()
Expand Down
5 changes: 3 additions & 2 deletions tests/command.copySettings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ describe('copyInheritedSettings property tests', () => {

source.helpOption('-Z, --zz', 'ddd');
cmd.copyInheritedSettings(source);
expect(cmd._hasHelpOption).toBeTruthy();
expect(cmd._helpFlags).toBe('-Z, --zz');
expect(cmd._helpDescription).toBe('ddd');
expect(cmd._helpShortFlag).toBe('-Z');
expect(cmd._helpLongFlag).toBe('--zz');
expect(cmd._helpOption.short).toBe('-Z');
expect(cmd._helpOption.long).toBe('--zz');
});

test('when copyInheritedSettings then copies addHelpCommand(name, description)', () => {
Expand Down
118 changes: 118 additions & 0 deletions tests/command.helpOption.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,121 @@ describe('helpOption', () => {
}).toThrow("error: unknown command 'UNKNOWN'");
});
});

describe('obscured help flags', () => {
test('when obscured default help short flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.option('-h');
expect(() => {
program.parse(['-h'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when obscured default help long flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.option('--help');
expect(() => {
program.parse(['--help'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when both default help flags obscured and short flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.option('-h, --help');
expect(() => {
program.parse(['-h'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when both default help flags obscured and long flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.option('-h, --help');
expect(() => {
program.parse(['--help'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when obscured custom help short flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.helpOption('-c, --custom-help')
.option('-c');
expect(() => {
program.parse(['-c'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when obscured custom help long flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.helpOption('-c, --custom-help')
.option('--custom-help');
expect(() => {
program.parse(['--custom-help'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when both custom help flags obscured and short flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.helpOption('-c, --custom-help')
.option('-c, --custom-help');
expect(() => {
program.parse(['-c'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});

test('when both custom help flags obscured and long flag parsed then outputHelp() not called', () => {
const program = new commander.Command();
program.outputHelp = jest.fn().mockImplementation(
program.outputHelp.bind(program)
);
program
.exitOverride()
.helpOption('-c, --custom-help')
.option('-c, --custom-help');
expect(() => {
program.parse(['--custom-help'], { from: 'user' });
}).not.toThrow();
expect(program.outputHelp).not.toHaveBeenCalled();
});
});
48 changes: 24 additions & 24 deletions tests/command.parseOptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,97 +61,97 @@ describe('parseOptions', () => {
test('when empty args then empty results', () => {
const program = createProgram();
const result = program.parseOptions([]);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
});

test('when only operands then results has all operands', () => {
const program = createProgram();
const result = program.parseOptions('one two three'.split(' '));
expect(result).toEqual({ operands: ['one', 'two', 'three'], unknown: [] });
expect(result).toEqual({ operands: ['one', 'two', 'three'], unknown: [], displayHelp: false });
});

test('when subcommand and operand then results has subcommand and operand', () => {
const program = createProgram();
const result = program.parseOptions('sub one'.split(' '));
expect(result).toEqual({ operands: ['sub', 'one'], unknown: [] });
expect(result).toEqual({ operands: ['sub', 'one'], unknown: [], displayHelp: false });
});

test('when program has flag then option removed', () => {
const program = createProgram();
const result = program.parseOptions('--global-flag'.split(' '));
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
});

test('when program has option with value then option removed', () => {
const program = createProgram();
const result = program.parseOptions('--global-value foo'.split(' '));
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
});

test('when program has flag before operand then option removed', () => {
const program = createProgram();
const result = program.parseOptions('--global-flag arg'.split(' '));
expect(result).toEqual({ operands: ['arg'], unknown: [] });
expect(result).toEqual({ operands: ['arg'], unknown: [], displayHelp: false });
});

test('when program has flag after operand then option removed', () => {
const program = createProgram();
const result = program.parseOptions('arg --global-flag'.split(' '));
expect(result).toEqual({ operands: ['arg'], unknown: [] });
expect(result).toEqual({ operands: ['arg'], unknown: [], displayHelp: false });
});

test('when program has flag after subcommand then option removed', () => {
const program = createProgram();
const result = program.parseOptions('sub --global-flag'.split(' '));
expect(result).toEqual({ operands: ['sub'], unknown: [] });
expect(result).toEqual({ operands: ['sub'], unknown: [], displayHelp: false });
});

test('when program has unknown option then option returned in unknown', () => {
const program = createProgram();
const result = program.parseOptions('--unknown'.split(' '));
expect(result).toEqual({ operands: [], unknown: ['--unknown'] });
expect(result).toEqual({ operands: [], unknown: ['--unknown'], displayHelp: false });
});

test('when program has unknown option before operands then all unknown in same order', () => {
const program = createProgram();
const result = program.parseOptions('--unknown arg'.split(' '));
expect(result).toEqual({ operands: [], unknown: ['--unknown', 'arg'] });
expect(result).toEqual({ operands: [], unknown: ['--unknown', 'arg'], displayHelp: false });
});

test('when program has unknown option after operand then option returned in unknown', () => {
const program = createProgram();
const result = program.parseOptions('arg --unknown'.split(' '));
expect(result).toEqual({ operands: ['arg'], unknown: ['--unknown'] });
expect(result).toEqual({ operands: ['arg'], unknown: ['--unknown'], displayHelp: false });
});

test('when program has flag after unknown option then flag removed', () => {
const program = createProgram();
const result = program.parseOptions('--unknown --global-flag'.split(' '));
expect(result).toEqual({ operands: [], unknown: ['--unknown'] });
expect(result).toEqual({ operands: [], unknown: ['--unknown'], displayHelp: false });
});

test('when subcommand has flag then flag returned as unknown', () => {
const program = createProgram();
const result = program.parseOptions('sub --sub-flag'.split(' '));
expect(result).toEqual({ operands: ['sub'], unknown: ['--sub-flag'] });
expect(result).toEqual({ operands: ['sub'], unknown: ['--sub-flag'], displayHelp: false });
});

test('when program has literal before known flag then option returned as operand', () => {
const program = createProgram();
const result = program.parseOptions('-- --global-flag'.split(' '));
expect(result).toEqual({ operands: ['--global-flag'], unknown: [] });
expect(result).toEqual({ operands: ['--global-flag'], unknown: [], displayHelp: false });
});

test('when program has literal before unknown option then option returned as operand', () => {
const program = createProgram();
const result = program.parseOptions('-- --unknown uuu'.split(' '));
expect(result).toEqual({ operands: ['--unknown', 'uuu'], unknown: [] });
expect(result).toEqual({ operands: ['--unknown', 'uuu'], unknown: [], displayHelp: false });
});

test('when program has literal after unknown option then literal preserved too', () => {
const program = createProgram();
const result = program.parseOptions('--unknown1 -- --unknown2'.split(' '));
expect(result).toEqual({ operands: [], unknown: ['--unknown1', '--', '--unknown2'] });
expect(result).toEqual({ operands: [], unknown: ['--unknown1', '--', '--unknown2'], displayHelp: false });
});
});

Expand Down Expand Up @@ -236,56 +236,56 @@ describe('Utility Conventions', () => {
test('when program has combo known boolean short flags then arg removed', () => {
const program = createProgram();
const result = program.parseOptions(['-ab']);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
expect(program.opts()).toEqual({ aaa: true, bbb: true });
});

test('when program has combo unknown short flags then arg preserved', () => {
const program = createProgram();
const result = program.parseOptions(['-pq']);
expect(result).toEqual({ operands: [], unknown: ['-pq'] });
expect(result).toEqual({ operands: [], unknown: ['-pq'], displayHelp: false });
expect(program.opts()).toEqual({ });
});

test('when program has combo known short option and required value then arg removed', () => {
const program = createProgram();
const result = program.parseOptions(['-cvalue']);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
expect(program.opts()).toEqual({ ccc: 'value' });
});

test('when program has combo known short option and optional value then arg removed', () => {
const program = createProgram();
const result = program.parseOptions(['-dvalue']);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
expect(program.opts()).toEqual({ ddd: 'value' });
});

test('when program has known combo short boolean flags and required value then arg removed', () => {
const program = createProgram();
const result = program.parseOptions(['-abcvalue']);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
expect(program.opts()).toEqual({ aaa: true, bbb: true, ccc: 'value' });
});

test('when program has known combo short boolean flags and optional value then arg removed', () => {
const program = createProgram();
const result = program.parseOptions(['-abdvalue']);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
expect(program.opts()).toEqual({ aaa: true, bbb: true, ddd: 'value' });
});

test('when program has known long flag=value then arg removed', () => {
const program = createProgram();
const result = program.parseOptions(['--ccc=value']);
expect(result).toEqual({ operands: [], unknown: [] });
expect(result).toEqual({ operands: [], unknown: [], displayHelp: false });
expect(program.opts()).toEqual({ ccc: 'value' });
});

test('when program has unknown long flag=value then arg preserved', () => {
const program = createProgram();
const result = program.parseOptions(['--rrr=value']);
expect(result).toEqual({ operands: [], unknown: ['--rrr=value'] });
expect(result).toEqual({ operands: [], unknown: ['--rrr=value'], displayHelp: false });
expect(program.opts()).toEqual({ });
});

Expand Down