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

Help groups for commands and options #1908

Closed
wants to merge 8 commits into from
Closed
80 changes: 80 additions & 0 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ class Command extends EventEmitter {
outputError: (str, write) => write(str)
};

this._group = undefined;
this._defaultCommandGroup = undefined;
this._defaultOptionGroup = undefined;
/** @type Map<string, string[]> */
this.commandGroups = new Map();
/** @type Map<string, string[]> */
this.optionGroups = new Map();

this._hidden = false;
this._hasHelpOption = true;
this._helpFlags = '-h, --help';
Expand Down Expand Up @@ -154,6 +162,7 @@ class Command extends EventEmitter {
cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor
if (args) cmd.arguments(args);
this.commands.push(cmd);
this._addGroupCommand(this._defaultCommandGroup, cmd.name());
cmd.parent = this;
cmd.copyInheritedSettings(this);

Expand Down Expand Up @@ -271,6 +280,7 @@ class Command extends EventEmitter {
if (opts.noHelp || opts.hidden) cmd._hidden = true; // modifying passed command due to existing implementation

this.commands.push(cmd);
this._addGroupCommand(this._defaultCommandGroup, cmd.name());
cmd.parent = this;
return this;
}
Expand Down Expand Up @@ -374,6 +384,7 @@ class Command extends EventEmitter {
this._helpCommandnameAndArgs = enableOrNameAndArgs;
}
this._helpCommandDescription = description || this._helpCommandDescription;
this._addGroupCommand(this._defaultCommandGroup, this._helpCommandName);
}
return this;
}
Expand Down Expand Up @@ -523,6 +534,7 @@ Expecting one of '${allowedValues.join("', '")}'`);

// register the option
this.options.push(option);
this._addGroupOption(this._defaultOptionGroup, option);

// handler for cli and env supplied values
const handleOptionValue = (val, invalidValueMessage, valueSource) => {
Expand Down Expand Up @@ -1821,6 +1833,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
const versionOption = this.createOption(flags, description);
this._versionOptionName = versionOption.attributeName();
this.options.push(versionOption);
this._addGroupOption(this._defaultOptionGroup, versionOption);
this.on('option:' + versionOption.name(), () => {
this._outputConfiguration.writeOut(`${str}\n`);
this._exit(0, 'commander.version', str);
Expand Down Expand Up @@ -1936,6 +1949,69 @@ Expecting one of '${allowedValues.join("', '")}'`);
return this;
}

/**
* Get or set the help group of the command.
*
* @param {string} [title]
* @return {string|Command}
*/

group(title) {
if (title === undefined) return this._group;
this._group = title;
return this;
}

/**
*
* @param {string} title
*/
startCommandGroup(title) {
this._defaultCommandGroup = title;
this._addGroupCommand(title);
return this;
}

/**
*
* @param {string} group
* @param {Command} name
* @api private;
*/
_addGroupCommand(group, commandName) {
if (!group) return; // cmd.group gets handled later
if (!this.commandGroups.has(group)) this.commandGroups.set(group, []);
if (commandName) this.commandGroups.get(group).push(commandName);
}

/**
*
* @param {string} title
* @api private;
*/
startOptionGroup(title) {
this._defaultOptionGroup = title;
this._addGroupOption(title);
return this;
}

/**
*
* @param {string} group
* @param {Option | string} [option] - allow string to make it easy to add implicit help option
* @api private;
*/
_addGroupOption(group, option) {
if (!group) return; // option.group gets handled later
if (!this.optionGroups.has(group)) this.optionGroups.set(group, []);
if (option) {
const optionKey = typeof option === 'string'
? option
: option.long ?? option.short;
this.optionGroups.get(group).push(optionKey);
}
}

/**
* Set the name of the command from script filename, such as process.argv[1],
* or require.main.filename, or __filename.
Expand Down Expand Up @@ -2052,6 +2128,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
helpOption(flags, description) {
if (typeof flags === 'boolean') {
this._hasHelpOption = flags;
// This is a bit messy:
if (this._hasHelpOption) this._addGroupOption(this._defaultOptionGroup, this._helpLongFlag ?? this._helpShortFlag);
return this;
}
this._helpFlags = flags || this._helpFlags;
Expand All @@ -2060,6 +2138,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
const helpFlags = splitOptionFlags(this._helpFlags);
this._helpShortFlag = helpFlags.shortFlag;
this._helpLongFlag = helpFlags.longFlag;
// This is a bit messy:
this._addGroupOption(this._defaultOptionGroup, this._helpLongFlag ?? this._helpShortFlag);

return this;
}
Expand Down
105 changes: 94 additions & 11 deletions lib/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,87 @@ class Help {
return visibleCommands;
}

/**
* info.defaultTitle
* info.overrideMap
* info.things
* info.visibleThings
* info.match
*
* @api private
*/
_visibleThingGroups(cmd, helper, info) {
const groupMap = new Map();
const addGroup = (group) => {
if (group && !groupMap.has(group)) groupMap.set(group, []);
};

// Add groups from override map in order groups created.
for (const group of info.overrideMap.keys()) addGroup(group);
// Add groups from things in order things created.
info.things.forEach(thing => addGroup(thing.group()));
// Add default last, but may have been added above.
addGroup(info.defaultTitle);

// Process visible things into groups (reminder: may be sorted).
info.visibleThings.forEach(thing => {
let overrideFound = false;
info.overrideMap.forEach((things, group) => {
if (things.find(key => info.match(key, thing))) {
groupMap.get(group).push(thing);
overrideFound = true;
}
});
if (!overrideFound) {
const group = thing.group() ?? info.defaultTitle;
groupMap.get(group).push(thing);
}
});

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

return groupMap;
}

/**
* Make public?
* @param {Command} cmd
* @param {Help} helper
* @return {Map<string, Command[]>}
* @api private
*/
_visibleCommandGroups(cmd, helper) {
return this._visibleThingGroups(cmd, helper, {
defaultTitle: 'Commands:',
overrideMap: cmd.commandGroups ?? new Map(),
things: cmd.commands,
visibleThings: helper.visibleCommands(cmd),
match: (id, sub) => id === sub.name()
});
}

/**
* Make public?
* @param {Command} cmd
* @param {Help} helper
* @return {Map<string, Option[]>}
* @api private
*/
_visibleOptionGroups(cmd, helper) {
return this._visibleThingGroups(cmd, helper, {
defaultTitle: 'Options:',
overrideMap: cmd.optionGroups ?? new Map(),
things: cmd.options,
visibleThings: helper.visibleOptions(cmd),
match: (id, opt) => opt.is(id) || id === opt.attributeName()
});
}

/**
* Compare options for sort.
*
Expand Down Expand Up @@ -379,12 +460,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 +477,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
14 changes: 14 additions & 0 deletions lib/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Option {
this.envVar = undefined;
this.parseArg = undefined;
this.hidden = false;
this.helpGroup = undefined;
this.argChoices = undefined;
this.conflictsWith = [];
this.implied = undefined;
Expand Down Expand Up @@ -159,6 +160,19 @@ class Option {
return this;
}

/**
* Get or set the help group of the option.
*
* @param {string} title
* @return {Option}
*/

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

/**
* @api private
*/
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 call .group() then returns this', () => {
const program = new Command();
const result = program.group('My Commands:');
expect(result).toBe(program);
});
});
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 call .group() then returns this', () => {
const option = new Option('-e,--example <value>');
const result = option.group('My Options:');
expect(result).toBe(option);
});
});