-
Notifications
You must be signed in to change notification settings - Fork 422
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
usage help for nested sub listing full command tree #566
Comments
Currently this may not be easy to achieve. There is an item (#530) on the todo list that will facilitate customizations like this. An implementation of the ideas in #530 should make it possible for application authors to specify a custom Application authors would control these section renderers via a This work is planned for picocli 4.0. If you’re interested in helping out with this that’d be great, please let me know. |
Update: there's a very promising PR in progress (#567) that will facilitate the above customization. If things go well I can do a 3.9 release with this. |
#530 (custom usage help message section renderers) has been merged to master. |
This would be a very appreciated addition. |
I've added an example that shows how to accomplish this customization with the new API introduced in picocli 3.9: package picocli.examples.customhelp;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Help;
import picocli.CommandLine.Help.Column;
import picocli.CommandLine.Help.Column.Overflow;
import picocli.CommandLine.Help.TextTable;
import picocli.CommandLine.IHelpSectionRenderer;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.UsageMessageSpec;
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
/**
* This example demonstrates how to customize a section of the usage help message.
* It replaces the standard command list with a custom list that displays
* not just the immediate subcommands but the full hierarchy of subcommands:
* <pre>
* Usage: showall [-hV] [COMMAND]
* Demonstrates a usage help message that shows not just the subcommands of this
* command, but also the nested sub-subcommands.
* -h, --help Show this help message and exit.
* -V, --version Print version information and exit.
* Commands:
* sub1 subcommand1 of showall
* sub1sub1 subcommand1 of subcommand1 of showall
* sub1sub2 subcommand2 of subcommand1 of showall
* sub2 subcommand2 of showall
* sub2sub1 subcommand1 of subcommand2 of showall
* </pre>
*
* As requested in https://github.com/remkop/picocli/issues/566
*/
@Command(name = "showall", mixinStandardHelpOptions = true,
version = "from picocli 3.9",
description = "Demonstrates a usage help message that shows " +
"not just the subcommands of this command, " +
"but also the nested sub-subcommands.",
subcommands = {Subcommand1.class, Subcommand2.class} )
public class ShowAll {
public static void main(String[] args) {
CommandLine cmd = new CommandLine(new ShowAll());
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new MyCommandListRenderer());
cmd.usage(System.out);
}
}
class MyCommandListRenderer implements IHelpSectionRenderer {
@Override
public String render(Help help) {
CommandSpec spec = help.commandSpec();
if (spec.subcommands().isEmpty()) { return ""; }
// prepare layout: two columns
// the left column overflows, the right column wraps if text is too long
TextTable textTable = TextTable.forColumns(help.ansi(),
new Column(15, 2, Overflow.SPAN),
new Column(spec.usageMessage().width() - 15, 2, Overflow.WRAP));
for (CommandLine subcommand : spec.subcommands().values()) {
addHierarchy(subcommand, textTable, "");
}
return textTable.toString();
}
private void addHierarchy(CommandLine cmd, TextTable textTable, String indent) {
// create comma-separated list of command name and aliases
String names = cmd.getCommandSpec().names().toString();
names = names.substring(1, names.length() - 1); // remove leading '[' and trailing ']'
// command description is taken from header or description
String description = description(cmd.getCommandSpec().usageMessage());
// add a line for this command to the layout
textTable.addRowValues(indent + names, description);
// add its subcommands (if any)
for (CommandLine sub : cmd.getSubcommands().values()) {
addHierarchy(sub, textTable, indent + " ");
}
}
private String description(UsageMessageSpec usageMessage) {
if (usageMessage.header().length > 0) {
return usageMessage.header()[0];
}
if (usageMessage.description().length > 0) {
return usageMessage.description()[0];
}
return "";
}
}
@Command(name = "sub1", description = "subcommand1 of showall",
subcommands = {Subcommand1Sub1.class, Subcommand1Sub2.class})
class Subcommand1 {}
@Command(name = "sub2", description = "subcommand2 of showall",
subcommands = {Subcommand2Sub1.class})
class Subcommand2 {}
@Command(name = "sub1sub1", description = "subcommand1 of subcommand1 of showall")
class Subcommand1Sub1 {}
@Command(name = "sub1sub2", description = "subcommand2 of subcommand1 of showall")
class Subcommand1Sub2 {}
@Command(name = "sub2sub1", description = "subcommand1 of subcommand2 of showall")
class Subcommand2Sub1 {} |
@lgawron, can you check if the above meets your needs? |
I will do so and get back to you soon
…On Fri, Jan 4, 2019, 09:31 Remko Popma ***@***.***> wrote:
@lgawron <https://github.com/lgawron>, can you check if the above meets
your needs?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#566 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AApFh29NC4iEJBB7gPxpP3AqSiLuMu6tks5u_xFrgaJpZM4ZLGvO>
.
|
…owing how to customize the usage help message
I went ahead and released picocli-3.9.0. :-) I believe the new API should enable you to make all kinds of customizations in the usage help message, including the requested one. Please let me know how it is working for you. Also, suggestions for further improvements are always welcome! |
Yay! 🎉 |
So I'm trying to give this a spin and I'm hitting up against not having enough context in |
I was able to reproduce with this: static class HelpCommandRender implements IHelpSectionRenderer {
private final CommandLine commandLine;
public HelpCommandRender(CommandLine commandLine) {
this.commandLine = commandLine;
}
@Override
public String render(Help parentHelp) {
Map<String, CommandLine> subcommands = commandLine.getSubcommands();
if (subcommands.isEmpty()) {
return "";
}
TextTable textTable = createCommandTable(parentHelp, subcommands);
subcommands.values().stream().forEach(command -> renderHelp(command, parentHelp, textTable));
return textTable.toString();
}
protected TextTable createCommandTable(Help parentHelp, Map<String, CommandLine> subcommands) {
int commandLength = maxLength(subcommands.keySet());
return TextTable.forColumns(parentHelp.ansi(),
new Column(commandLength + 2, 2, Column.Overflow.SPAN),
new Column(commandLine.getUsageHelpWidth() - (commandLength + 2), 2, Column.Overflow.WRAP));
}
private void renderHelp(CommandLine command, Help parentHelp, TextTable textTable) {
Help help = command.getHelpFactory().create(command.getCommandSpec(), parentHelp.colorScheme());
UsageMessageSpec usage = help.commandSpec().usageMessage();
String header = !empty(usage.header()) ? usage.header()[0] : (!empty(usage.description()) ? usage.description()[0] : "");
Text[] lines = help.ansi().text(format(header)).splitLines();
for (int i = 0; i < lines.length; i++) {
textTable.addRowValues(i == 0 ? help.commandNamesText(", ") : parentHelp.ansi().text(""), lines[i]);
}
}
private static String format(String formatString, Object... params) {
return formatString == null ? "" : String.format(formatString, params);
}
private static boolean empty(Object[] array) {
return array == null || array.length == 0;
}
private static int maxLength(Collection<String> any) {
List<String> strings = new ArrayList<String>(any);
Collections.sort(strings, Collections.reverseOrder(Help.shortestFirst()));
return strings.get(0).length();
}
} Is this what one would need to do today? Or am I missing something? |
It is possible to pass in additional context like you did, but you should be able to get all you need from the passed-in For example, you can get the It’s unfortunate that you have to construct |
Looks like semver says that bumping visibility of the |
I couldn't subclass help because it required a command object which I didn't have in this context. For now I'm just doing a more customized version of the above approach: /**
* Prints command listing help to terminal with categorized sections.
*/
static class HelpRenderer implements IHelpSectionRenderer {
private final Map<String, String> sections;
HelpRenderer(Map<String, String> sections) {
this.sections = sections;
}
@Override
public String render(Help help) {
CommandLine commandLine = help.commandSpec().commandLine();
Map<String, CommandLine> subcommands = commandLine.getSubcommands();
if (subcommands.isEmpty()) {
return "";
}
TextTable commandsTable = createCommandsTable(help, subcommands);
for (CommandLine subCommandLine : subcommands.values()) {
String sectionName = sections.get(subCommandLine.getCommandName());
if (sectionName != null) {
commandsTable.addEmptyRow();
commandsTable.addRowValues(formatSection(sectionName));
}
addCommandHelp(commandsTable, getHelp(subCommandLine, help.colorScheme()));
}
return commandsTable.toString();
}
private static TextTable createCommandsTable(Help parentHelp, Map<String, CommandLine> subcommands) {
int commandLength = maxLength(subcommands.keySet());
return TextTable.forColumns(parentHelp.ansi(),
new Column(commandLength + 2, 2, Overflow.SPAN),
new Column(width(parentHelp) - (commandLength + 2), 2, Overflow.WRAP));
}
private static void addCommandHelp(TextTable commandsTable, Help help) {
UsageMessageSpec usage = help.commandSpec().usageMessage();
String header = !isEmpty(usage.header()) ? usage.header()[0] : !isEmpty(usage.description()) ? usage.description()[0] : "";
Text[] lines = help.ansi().text(format(header)).splitLines();
for (int i = 0; i < lines.length; i++) {
commandsTable.addRowValues(i == 0 ? help.commandNamesText(", ") : help.ansi().text(""), lines[i]);
}
}
private static Help getHelp(CommandLine command, ColorScheme colorScheme) {
return command.getHelpFactory().create(command.getCommandSpec(), colorScheme);
}
private static int width(Help help) {
return help.commandSpec().commandLine().getUsageHelpWidth();
}
private static String formatSection(String sectionName) {
return "@|" + Theme.SECTION + ",bold " + sectionName + "|@";
}
private static String format(String formatString, Object... params) {
return formatString == null ? "" : String.format(formatString, params);
}
private static int maxLength(Collection<String> any) {
return any.stream()
.sorted(reverseOrder(Help.shortestFirst()))
.findFirst()
.orElse("")
.length();
}
} Btw, is there any plans to add annotation based versions of sections? I seem to recall that request in another issue. |
There is a plan to support Is that what you have in mind? |
You can access protected methods in public class Example {
public static void main(String[] args) {
CommandLine cmd = new CommandLine(new Example());
cmd.setHelpFactory(new MyHelp.Factory());
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new MyCommandListRenderer());
cmd.usage(System.out);
}
}
class MyHelp extends Help {
public MyHelp(CommandSpec commandSpec, ColorScheme colorScheme) {
super(commandSpec, colorScheme);
}
static class Factory implements CommandLine.IHelpFactory {
public Help create(CommandSpec commandSpec, ColorScheme colorScheme) {
return new MyHelp(commandSpec, colorScheme);
}
}
static class MyRenderer implements IHelpSectionRenderer {
public String render(Help help) {
Map<String, Help> subcommands = ((MyHelp) help).subcommands(); // access protected method
return "do stuff...";
}
}
} |
Thanks! I think this or my approach is an acceptable workaround for now. Although both suffer from some boilerplate so would be nice to trim the fat eventually. |
Hi,
anything with alias is listed twice .... actually if you specify 2 aliases it is listed 3 times. |
@lgawron Ya, for my case I was able to make a shortcut. Certainly my approach is not generalizable as is. I was going to create my own annotation to extract the metadata from within the renderer. That would make it more explicit. You should be able to tweak the map lookup logic to get the semantics you need though. |
ok, I was not aware that command with aliases is registered under each name separately
that did it. I should probably use some identity hash map but I do not want to introduce another dependency. Thanks! |
Great, thank you both for the verification and the feedback! Feel free to re-open or raise a new ticket if there's any problem. |
One more question: this works:
so the top level help command, but triggering help some node down the command tree does not:
as you see the |
I suspect you are doing this:
This only impacts the usage help of that command. To impact the whole hierarchy, you need to replace the map:
The convention in picocli is that calling a setter method on |
Not sure if this is related to this request (I thought it was originally), but would it be possible to add support to <command> help x y z Currently it looks like it simply resolves |
That should already work correctly when users invoke this:
I recommend that all commands in the hierarchy have |
Hmm, isn't it more typical the way I mentioned? An example that comes to mind is Kubernete's
either way, it seems like it could be supported pretty easily |
I'm happy with the way That said, I don't object to doing this. If anyone submits a quality PR that implements this (with tests and docs) I don't see any reason not to merge it. |
Currently invoking usageHelp for cli with nested subcommands shows only one level of commands.
Would it be possible to generate a full command listing like:
Commands:
and so on?
The text was updated successfully, but these errors were encountered: