-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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 support for custom help groups #1897
Comments
Thanks for the example, and creative partial work-around. I see option and command groups in "big" utilities and have wondered about grouping or separator support in Commander, but have not progressed to ideas. I had one related old comment saved: |
I did some research looking for prior art for adding options groups in the help. I found a few variations: Python Yargs has command-line-args has a Oclif options have a |
Yargs still has an open issue to group commands. The groups only work for flags there I believe. In the meanwhile, I've improved my work-around a bit, by adding a As you might see, I've customised the help quite a bit now. I wasn't looking for that, that's just a side effect of writing the custom help formatter like this 🙂. Also, if there's any interest in this work, I'd be happy to submit a pull-request. If not, no hard feelings. I'm happy enough with my current solution. createCommandexport function createCommand(name?: string): ExtendedCommand {
const command = new Command(name) as ExtendedCommand;
command.helpOption('-h, --help', 'Show help for command');
command.showHelpAfterError(true);
command.addHelpCommand(false);
command.configureHelp({
sortSubcommands: true,
showGlobalOptions: true,
subcommandTerm: (cmd) => cmd.name(),
// with callback support, this could look like formatHelp((sections) => generateHelpString(sections))
formatHelp: formatHelp,
});
command.group = (group) => {
(command as any)._group = group;
return command;
};
return command;
} formatHelpimport { Command, Help } from 'commander';
import kleur from 'kleur';
export function formatHelp(cmd: Command, helper: Help) {
const termWidth = helper.padWidth(cmd, helper);
const helpWidth = helper.helpWidth || 80;
const itemSeparatorWidth = 2; // between term and description
const indent = ' '.repeat(2);
const moveOptions = !cmd.parent && cmd.commands.length;
function formatItem(term, description) {
if (description) {
const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
return helper.wrap(fullText, helpWidth - indent.length, termWidth + itemSeparatorWidth);
}
return term;
}
function formatList(textArray) {
const list = textArray.join('\n').replace(/^/gm, indent).trim();
return list ? indent + list : '';
}
const sections = {
description: '',
usage: '',
arguments: '',
options: '',
commands: [] as { title: string; list: string }[],
globalOptions: '',
};
sections.description = helper.commandDescription(cmd);
sections.usage = helper.commandUsage(cmd);
sections.arguments = formatList(
helper.visibleArguments(cmd).map((argument) => {
return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument));
}),
);
// Note: options might benefit from similar grouping as commands below, I just didn't need that (yet)
sections.options = formatList(
helper
.visibleOptions(cmd)
.filter((option) => {
// move --help to global options
if (cmd.parent && option.long === '--help') {
cmd.parent.addOption(option);
return false;
}
return true;
})
.map((option) => {
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
}),
);
// Commands
const commands: Record<string, Command[]> = {};
for (const command of helper.visibleCommands(cmd)) {
const group = ((command as any)._group || 'Commands').trim();
commands[group] = commands[group] || [];
commands[group].push(command);
}
sections.commands = Object.entries(commands).map(([title, commands]) => ({
title,
list: formatList(commands.map((cmd) => formatItem(helper.subcommandTerm(cmd), helper.subcommandDescription(cmd)))),
}));
sections.globalOptions = this.showGlobalOptions
? formatList(
helper
.visibleGlobalOptions(cmd)
.filter((option) => {
// don't return --version on sub commands
return option.long !== '--version';
})
.map((option) => {
return formatItem(helper.optionTerm(option), helper.optionDescription(option));
}),
)
: '';
// -------
// Everything below here could be wrapped in a callback, so formatHelp(cb) can be used as formatter
// -------
const output = [];
output.push(kleur.bold('Usage'), indent + sections.usage, '');
if (sections.arguments) {
output.push(kleur.bold('Arguments'), sections.arguments, '');
}
if (sections.options && !moveOptions) {
output.push(kleur.bold('Options'), sections.options, '');
}
if (sections.commands.length) {
sections.commands.forEach((section) => {
output.push(kleur.bold(section.title), section.list, '');
});
}
if (sections.options && moveOptions) {
output.push(kleur.bold('Options'), sections.options, '');
}
if (sections.globalOptions) {
output.push(kleur.bold('Global Options'), sections.globalOptions, '');
}
return output.join('\n');
} usagefor (const command of commands) {
program.addCommand(command.group('Resource commands'));
}
for (const command of otherCommands) {
program.addCommand(command.group('Other commands'));
} Result: |
Interesting, thanks. I was wondering about the order the command groups will appear in the help list. I think that implementation will give varying results depending on the alphabetical ordering of the commands? Probably the order they were first used is good enough. So could perhaps create an empty |
Oh interesting. I haven't thought about the order. It just "naturally" worked. But yeah, when I'd drop the |
I was suggesting could create empty groups using the raw commands (unsorted, so in creation order), then populate the groups in a scan through the visibleCommands. And ignore zero-length groups that turned out to be all hidden. So can still all be done in help generation. |
Playing with three different styles that fit into current API. The
Using with code sample from earlier comment:
Edit: |
Hmm, how to set the group for the built-in help command or help option? (I see in your implementation you turn off the help command and handle the help option specially.) |
The help command could use similar syntax to
Commander largely avoids referring to commands and options by name, but a name style API does support help naturally:
With support for "separator" style API, could perhaps allow:
|
This comment was marked as outdated.
This comment was marked as outdated.
I noticed there is a request for untitled help groups "hidden" in the referenced PR: |
I think it'd be helpful if we had a way to group commands and options, to create sections in
--help
.There's a prior issue to this; #78, but that's over 9 years old. I'm also aware of
addHelpText
(#1296), but when using that to group commands, it adds boilerplate due to the need of needing to "hide" commands, and later formatting and properly indenting help ourselves.What I wish to achieve is something like:
This separates 'config commands' from 'core commands'. If you'd run
stripe --help
orgh --help
you can see similar command grouping.At this moment, I achieve the above via:
That's doable, but I think it'd be nice to have native support for it. Something like:
The
group
can both serve as group title, and grouping key.The text was updated successfully, but these errors were encountered: