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 helpGroup for options and commands #1910

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 35 additions & 7 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ class Command extends EventEmitter {
this._helpDescription = 'display help for command';
this._helpShortFlag = '-h';
this._helpLongFlag = '--help';
this._helpOptionGroup = undefined;
this._addImplicitHelpCommand = undefined; // Deliberately undefined, not decided whether true or false
this._helpCommandName = 'help';
this._helpCommandnameAndArgs = 'help [command]';
this._helpCommandDescription = 'display help for command';
this._helpCommandGroup = undefined;
this._helpConfiguration = {};
this._helpGroupTitle = undefined;
}

/**
Expand Down Expand Up @@ -153,6 +156,7 @@ class Command extends EventEmitter {
cmd._hidden = !!(opts.noHelp || opts.hidden); // noHelp is deprecated old name for hidden
cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor
if (args) cmd.arguments(args);
if (opts.helpGroup) cmd.helpGroup(opts.helpGroup);
this.commands.push(cmd);
cmd.parent = this;
cmd.copyInheritedSettings(this);
Expand Down Expand Up @@ -361,19 +365,24 @@ class Command extends EventEmitter {
* addHelpCommand(false); // force off
* addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom details
*
* @param {boolean | string} enableOrNameAndArgs
* @param {string} [description]
* @param {object} [helpOpts]
* @param {string} [helpOpts.helpGroup]
* @return {Command} `this` command for chaining
*/

addHelpCommand(enableOrNameAndArgs, description) {
if (enableOrNameAndArgs === false) {
this._addImplicitHelpCommand = false;
addHelpCommand(enableOrNameAndArgs, description, helpOpts) {
if (typeof enableOrNameAndArgs === 'boolean') {
this._addImplicitHelpCommand = enableOrNameAndArgs;
} else {
this._addImplicitHelpCommand = true;
if (typeof enableOrNameAndArgs === 'string') {
if (enableOrNameAndArgs) {
this._helpCommandName = enableOrNameAndArgs.split(' ')[0];
this._helpCommandnameAndArgs = enableOrNameAndArgs;
}
this._helpCommandDescription = description || this._helpCommandDescription;
this._helpCommandGroup = helpOpts?.helpGroup;
}
return this;
}
Expand Down Expand Up @@ -1805,20 +1814,23 @@ Expecting one of '${allowedValues.join("', '")}'`);
* This method auto-registers the "-V, --version" flag
* which will print the version number when passed.
*
* You can optionally supply the flags and description to override the defaults.
* You can optionally supply the flags and description and helpFlag to override the defaults.
*
* @param {string} str
* @param {string} [flags]
* @param {string} [description]
* @param {Object} [versionOpts]
* @param {string} [versionOpts.helpGroup]
* @return {this | string} `this` command for chaining, or version string if no arguments
*/

version(str, flags, description) {
version(str, flags, description, versionOpts) {
if (str === undefined) return this._version;
this._version = str;
flags = flags || '-V, --version';
description = description || 'output the version number';
const versionOption = this.createOption(flags, description);
if (versionOpts?.helpGroup) versionOption.helpGroup(versionOpts.helpGroup);
this._versionOptionName = versionOption.attributeName();
this.options.push(versionOption);
this.on('option:' + versionOption.name(), () => {
Expand Down Expand Up @@ -1936,6 +1948,19 @@ Expecting one of '${allowedValues.join("', '")}'`);
return this;
}

/**
* Get or set the help group of the command. Used as the title in the help. e.g. 'Commands:'
*
* @param {string} [title]
* @return {string|Command}
*/

helpGroup(title) {
if (title === undefined) return this._helpGroupTitle;
this._helpGroupTitle = title;
return this;
}

/**
* Set the name of the command from script filename, such as process.argv[1],
* or require.main.filename, or __filename.
Expand Down Expand Up @@ -2046,16 +2071,19 @@ Expecting one of '${allowedValues.join("', '")}'`);
*
* @param {string | boolean} [flags]
* @param {string} [description]
* @param {Object} [helpOpts]
* @param {string} [helpOpts.helpGroup]
* @return {Command} `this` command for chaining
*/

helpOption(flags, description) {
helpOption(flags, description, helpOpts) {
if (typeof flags === 'boolean') {
this._hasHelpOption = flags;
return this;
}
this._helpFlags = flags || this._helpFlags;
this._helpDescription = description || this._helpDescription;
this._helpOptionGroup = helpOpts?.helpGroup;

const helpFlags = splitOptionFlags(this._helpFlags);
this._helpShortFlag = helpFlags.shortFlag;
Expand Down
97 changes: 86 additions & 11 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Help {
.helpOption(false);
helpCommand.description(cmd._helpCommandDescription);
if (helpArgs) helpCommand.arguments(helpArgs);
if (cmd._helpCommandGroup) helpCommand.helpGroup(cmd._helpCommandGroup);
visibleCommands.push(helpCommand);
}
if (this.sortSubcommands) {
Expand Down Expand Up @@ -82,6 +83,7 @@ class Help {
} else {
helpOption = cmd.createOption(cmd._helpFlags, cmd._helpDescription);
}
if (cmd._helpOptionGroup) helpOption.helpGroup(cmd._helpOptionGroup);
visibleOptions.push(helpOption);
}
if (this.sortOptions) {
Expand Down Expand Up @@ -133,6 +135,77 @@ class Help {
return [];
}

/**
* @param {Command} cmd
* @param {Help} helper
* @param {Object} info
* @param {Option[] | Command[]} info.things
* @param {Option[] | Command[]} info.visibleThings
* @param {function} info.helpGroup
*
* @api private
*/
_visibleThingGroups(cmd, helper, info) {
const groupMap = new Map();
const ensureGroup = (group) => {
if (!groupMap.has(group)) {
groupMap.set(group, []);
}
};

// Create groups from things in order things created.
info.things.forEach(thing => {
const group = info.helpGroup(thing);
ensureGroup(group);
});

// Process visible things into groups (reminder: may be sorted, may be extra help item).
info.visibleThings.forEach(thing => {
const group = info.helpGroup(thing);
ensureGroup(group);
groupMap.get(group).push(thing);
});

// Remove empty groups
groupMap.forEach((things, key) => {
if (things.length === 0) {
groupMap.delete(key);
}
});

return groupMap;
}

/**
* Return map of Commands using group as key.
*
* @param {Command} cmd
* @param {Help} helper
* @return {Map<string, Command[]>}
*/
visibleCommandGroups(cmd, helper) {
return this._visibleThingGroups(cmd, helper, {
helpGroup: (cmd) => cmd.helpGroup() || 'Commands:',
things: cmd.commands,
visibleThings: helper.visibleCommands(cmd)
});
}

/**
* Return map of Options using group as key.
*
* @param {Command} cmd
* @param {Help} helper
* @return {Map<string, Option[]>}
*/
visibleOptionGroups(cmd, helper) {
return this._visibleThingGroups(cmd, helper, {
helpGroup: (option) => option.helpGroupTitle || 'Options:',
things: cmd.options,
visibleThings: helper.visibleOptions(cmd)
});
}

/**
* Get the command term to show in the list of subcommands.
*
Expand Down Expand Up @@ -379,12 +452,13 @@ class Help {
}

// Options
const optionList = helper.visibleOptions(cmd).map((option) => {
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
const optionGroups = this.visibleOptionGroups(cmd, helper);
optionGroups.forEach((opts, groupTitle) => {
const optionList = opts.map((opt) => {
return formatItem(helper.optionTerm(opt), helper.optionDescription(opt));
});
output = output.concat([groupTitle, formatList(optionList), '']);
});
if (optionList.length > 0) {
output = output.concat(['Options:', formatList(optionList), '']);
}

if (this.showGlobalOptions) {
const globalOptionList = helper.visibleGlobalOptions(cmd).map((option) => {
Expand All @@ -395,13 +469,14 @@ class Help {
}
}

// Commands
const commandList = helper.visibleCommands(cmd).map((cmd) => {
return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
// Command Groups
const commandGroups = this.visibleCommandGroups(cmd, helper);
commandGroups.forEach((cmds, groupTitle) => {
const commandList = cmds.map((cmd) => {
return formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd));
});
output = output.concat([groupTitle, formatList(commandList), '']);
});
if (commandList.length > 0) {
output = output.concat(['Commands:', formatList(commandList), '']);
}

return output.join('\n');
}
Expand Down
13 changes: 13 additions & 0 deletions lib/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Option {
this.argChoices = undefined;
this.conflictsWith = [];
this.implied = undefined;
this.helpGroupTitle = undefined;
}

/**
Expand Down Expand Up @@ -217,6 +218,18 @@ class Option {
return camelcase(this.name().replace(/^no-/, ''));
}

/**
* Set the help group of the option. Used as the title in the help. e.g. 'Options:'
*
* @param {string} [title]
* @return {string|Option}
*/

helpGroup(title) {
this.helpGroupTitle = title;
return this;
}

/**
* Check if `arg` matches the short or long flag.
*
Expand Down
6 changes: 6 additions & 0 deletions tests/command.chain.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,10 @@ describe('Command methods that should return this for chaining', () => {
const result = program.nameFromFilename('name');
expect(result).toBe(program);
});

test('when set .helpGroup() then returns this', () => {
const program = new Command();
const result = program.helpGroup('Commands:');
expect(result).toBe(program);
});
});
17 changes: 17 additions & 0 deletions tests/command.helpGroup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const commander = require('../');

// Just testing the observable Command behaviour here, actual Help behaviour tested elsewhere.

test('when set helpGroup on Command then can get helpGroup', () => {
const cmd = new commander.Command();
const group = 'Example:';
cmd.helpGroup(group);
expect(cmd.helpGroup()).toEqual(group);
});

test('when use opt.helpGroup with external command then sets helpGroup on new command', () => {
const program = new commander.Command();
const group = 'Example:';
program.command('external', 'external description', { helpGroup: group });
expect(program.commands[program.commands.length - 1].helpGroup()).toEqual(group);
});
66 changes: 66 additions & 0 deletions tests/help.helpGroup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const { Command, Option } = require('../');

test('when use cmd.helpGroup then command listed in custom group', () => {
const program = new Command();
program.command('foo')
.description('foo description')
.helpGroup('Example Command:')
.action(() => {});
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/Example Command:\n +foo +foo description\n\n/);
});

test('when use opt:helpGroup with external command then command listed in custom group', () => {
const program = new Command();
program.command('foo', 'foo description', { helpGroup: 'Example Command:' });
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/Example Command:\n +foo +foo description\n\n/);
});

test('when use opt:helpGroup with addCommand then command listed in custom group', () => {
const program = new Command();
program.addCommand(new Command('foo').description('foo description').helpGroup('Example Command:'));
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/Example Command:\n +foo +foo description\n\n/);
});

test('when hidden command with group then group not displayed', () => {
const program = new Command();
program.command('hidden', 'hidden description', { helpGroup: 'Hidden Command:', hidden: true });
const helpInfo = program.helpInformation();
expect(helpInfo).not.toMatch(/Hidden/);
});

test('when use opt:helpGroup with addHelpCommand then command listed in custom group', () => {
const program = new Command();
program.addHelpCommand('help', 'show help', { helpGroup: 'Example Command:' });
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/Example Command:\n +help +show help\n/);
});

test('when use opt:helpGroup with helpOption then option listed in custom group', () => {
const program = new Command();
program.helpOption('-h, --help', 'show help', { helpGroup: 'Example Option:' });
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/Example Option:\n +-h, --help +show help\n/);
});

test('when sort commands then command groups in creation order and commands sorted in group', () => {
const program = new Command();
program.configureHelp({ sortSubcommands: true });
program.command('z2', 'ZZ', { helpGroup: 'First Group:' });
program.command('a', 'AA', { helpGroup: 'Second Group:' });
program.command('z1', 'ZZ', { helpGroup: 'First Group:' });
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/First Group:\n +z1 +ZZ\n +z2 +ZZ\n\nSecond Group:/);
});

test('when sort options then options groups in creation order and options sorted in group', () => {
const program = new Command();
program.configureHelp({ sortOptions: true });
program.addOption(new Option('-z', 'ZZ').helpGroup('First Group:'));
program.addOption(new Option('-a', 'AA').helpGroup('Second Group:'));
program.addOption(new Option('-y', 'YY').helpGroup('First Group:'));
const helpInfo = program.helpInformation();
expect(helpInfo).toMatch(/First Group:\n +-y +YY\n +-z + ZZ\n\nSecond Group:/);
});
6 changes: 6 additions & 0 deletions tests/option.chain.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,10 @@ describe('Option methods that should return this for chaining', () => {
const result = option.conflicts(['a']);
expect(result).toBe(option);
});

test('when set .helpGroup() then returns this', () => {
const option = new Option('-e,--example <value>');
const result = option.helpGroup('Option:');
expect(result).toBe(option);
});
});
10 changes: 10 additions & 0 deletions tests/options.helpGroup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const commander = require('../');

// Just testing the observable Option behaviour here, actual Help behaviour tested elsewhere.

test('when set helpGroup then stored as helpGroupTitle', () => {
const opt = new commander.Option('-e, --example');
const group = 'Example:';
opt.helpGroup(group);
expect(opt.helpGroupTitle).toEqual(group);
});