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

[#530] Usage message customization (2) #567

Merged
merged 3 commits into from
Dec 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 170 additions & 29 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;

import java.util.stream.Collectors;
import picocli.CommandLine.Help.Ansi.IStyle;
import picocli.CommandLine.Help.Ansi.Style;
import picocli.CommandLine.Help.Ansi.Text;
Expand Down Expand Up @@ -140,14 +140,50 @@
* </p>
*/
public class CommandLine {

/** Predefined section keys. */
public static final String HEADER_HEADING = "headerHeading";
public static final String HEADER = "header";
public static final String SYNOPSIS_HEADING = "synopsisHeading";
public static final String SYNOPSIS = "synopsis";
public static final String DESCRIPTION_HEADING = "descriptionHeading";
public static final String DESCRIPTION = "description";
public static final String PARAMETER_LIST_HEADING = "parameterListHeading";
public static final String PARAMETER_LIST = "parameterList";
public static final String OPTION_LIST_HEADING = "optionListHeading";
public static final String OPTION_LIST = "optionList";
public static final String COMMAND_LIST_HEADING = "commandListHeading";
public static final String COMMAND_LIST = "commandList";
public static final String FOOTER_HEADING = "footerHeading";
public static final String FOOTER = "footer";

/** This is picocli version {@value}. */
public static final String VERSION = "4.0.0-SNAPSHOT";

private final Tracer tracer = new Tracer();
private final CommandSpec commandSpec;
private final Interpreter interpreter;
private final IFactory factory;

private IHelpFactory helpFactory;

private List<String> sectionKeys = Collections.unmodifiableList(Arrays.asList(
HEADER_HEADING,
HEADER,
SYNOPSIS_HEADING,
SYNOPSIS,
DESCRIPTION_HEADING,
DESCRIPTION,
PARAMETER_LIST_HEADING,
PARAMETER_LIST,
OPTION_LIST_HEADING,
OPTION_LIST,
COMMAND_LIST_HEADING,
COMMAND_LIST,
FOOTER_HEADING,
FOOTER));

private Map<String, IHelpSectionRenderer> helpSectionRendererMap = createHelpSectionRendererMap();

/**
* Constructs a new {@code CommandLine} interpreter with the specified object (which may be an annotated user object or a {@link CommandSpec CommandSpec}) and a default subcommand factory.
* <p>The specified object may be a {@link CommandSpec CommandSpec} object, or it may be a {@code @Command}-annotated
Expand Down Expand Up @@ -1467,6 +1503,18 @@ public static void usage(Object command, PrintStream out, Help.ColorScheme color
* @since 3.0 */
public void usage(PrintWriter writer, Help.Ansi ansi) { usage(writer, Help.defaultColorScheme(ansi)); }

public CommandLine setHelpFactory(IHelpFactory helpFactory) {
this.helpFactory = helpFactory;
return this;
}

public IHelpFactory getHelpFactory() {
if (helpFactory == null) {
helpFactory = new DefaultHelpFactory();
}
return helpFactory;
}

/**
* Prints a usage help message for the annotated command class to the specified {@code PrintStream}.
* Delegates construction of the usage help message to the {@link Help} inner class and is equivalent to:
Expand Down Expand Up @@ -1500,45 +1548,107 @@ public static void usage(Object command, PrintStream out, Help.ColorScheme color
* @param colorScheme the {@code ColorScheme} defining the styles for options, parameters and commands when ANSI is enabled
*/
public void usage(PrintStream out, Help.ColorScheme colorScheme) {
out.print(usage(new StringBuilder(), new Help(getCommandSpec(), colorScheme)));
out.print(usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), colorScheme)));
}
/** Similar to {@link #usage(PrintStream, Help.ColorScheme)}, but with the specified {@code PrintWriter} instead of a {@code PrintStream}.
* @since 3.0 */
public void usage(PrintWriter writer, Help.ColorScheme colorScheme) {
writer.print(usage(new StringBuilder(), new Help(getCommandSpec(), colorScheme)));
writer.print(usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), colorScheme)));
}
/** Similar to {@link #usage(PrintStream)}, but returns the usage help message as a String instead of printing it to the {@code PrintStream}.
* @since 3.2 */
public String getUsageMessage() {
return usage(new StringBuilder(), new Help(getCommandSpec())).toString();
return usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), Help.defaultColorScheme(Help.Ansi.AUTO))).toString();
}
/** Similar to {@link #usage(PrintStream, Help.Ansi)}, but returns the usage help message as a String instead of printing it to the {@code PrintStream}.
* @since 3.2 */
public String getUsageMessage(Help.Ansi ansi) {
return usage(new StringBuilder(), new Help(getCommandSpec(), ansi)).toString();
return usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), Help.defaultColorScheme(ansi))).toString();
}
/** Similar to {@link #usage(PrintStream, Help.ColorScheme)}, but returns the usage help message as a String instead of printing it to the {@code PrintStream}.
* @since 3.2 */
public String getUsageMessage(Help.ColorScheme colorScheme) {
return usage(new StringBuilder(), new Help(getCommandSpec(), colorScheme)).toString();
}
private static StringBuilder usage(StringBuilder sb, Help help) {
return sb.append(help.headerHeading())
.append(help.header())
.append(help.synopsisHeading()) //e.g. Usage:
.append(help.synopsis(help.synopsisHeadingLength())) //e.g. &lt;main class&gt; [OPTIONS] &lt;command&gt; [COMMAND-OPTIONS] [ARGUMENTS]
.append(help.descriptionHeading()) //e.g. %nDescription:%n%n
.append(help.description()) //e.g. {"Converts foos to bars.", "Use options to control conversion mode."}
.append(help.parameterListHeading()) //e.g. %nPositional parameters:%n%n
.append(help.parameterList()) //e.g. [FILE...] the files to convert
.append(help.optionListHeading()) //e.g. %nOptions:%n%n
.append(help.optionList()) //e.g. -h, --help displays this help and exits
.append(help.commandListHeading()) //e.g. %nCommands:%n%n
.append(help.commandList()) //e.g. add adds the frup to the frooble
.append(help.footerHeading())
.append(help.footer());
return usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), colorScheme)).toString();
}

private StringBuilder usage(StringBuilder sb, Help help) {
for (String key : getSectionKeys()) {
IHelpSectionRenderer renderer = helpSectionRendererMap.get(key);
if (renderer != null) { sb.append(renderer.render(help)); }
}
return sb;
}

/**
* Returns the section keys in the order that the usage help message should render the sections.
* This ordering may be modified with {@link #setSectionKeys(List) setSectionKeys}. The default keys are:
* <pre>
* "headerHeading",
* "header",
* "synopsisHeading",
* "synopsis",
* "descriptionHeading",
* "description",
* "parameterListHeading",
* "parameterList",
* "optionListHeading",
* "optionList",
* "commandListHeading",
* "commandList",
* "footerHeading",
* "footer"
* </pre>
* @since 3.9
*/
public List<String> getSectionKeys() { return sectionKeys; }

/**
* Sets the section keys in the order that the usage help message should render the sections.
* @see #getSectionKeys
* @since 3.9
*/
public void setSectionKeys(List<String> keys) { sectionKeys = Collections.unmodifiableList(keys); }

/** Returns the help section renderers for the predefined section keys. see: {@link #getSectionKeys()} */
private Map<String, IHelpSectionRenderer> createHelpSectionRendererMap() {
Map<String, IHelpSectionRenderer> result = new HashMap<String, IHelpSectionRenderer>();

result.put(HEADER_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.headerHeading(); } });
result.put(HEADER, new IHelpSectionRenderer() { public String render(Help help) { return help.header(); } });
//e.g. Usage:
result.put(SYNOPSIS_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.synopsisHeading(); } });
//e.g. &lt;main class&gt; [OPTIONS] &lt;command&gt; [COMMAND-OPTIONS] [ARGUMENTS]
result.put(SYNOPSIS, new IHelpSectionRenderer() { public String render(Help help) { return help.synopsis(help.synopsisHeadingLength()); } });
//e.g. %nDescription:%n%n
result.put(DESCRIPTION_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.descriptionHeading(); } });
//e.g. {"Converts foos to bars.", "Use options to control conversion mode."}
result.put(DESCRIPTION, new IHelpSectionRenderer() { public String render(Help help) { return help.description(); } });
//e.g. %nPositional parameters:%n%n
result.put(PARAMETER_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.parameterListHeading(); } });
//e.g. [FILE...] the files to convert
result.put(PARAMETER_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.parameterList(); } });
//e.g. %nOptions:%n%n
result.put(OPTION_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.optionListHeading(); } });
//e.g. -h, --help displays this help and exits
result.put(OPTION_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.optionList(); } });
//e.g. %nCommands:%n%n
result.put(COMMAND_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.commandListHeading(); } });
//e.g. add adds the frup to the frooble
result.put(COMMAND_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.commandList(); } });
result.put(FOOTER_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.footerHeading(); } });
result.put(FOOTER, new IHelpSectionRenderer() { public String render(Help help) { return help.footer(); } });
return result;
}

/**
* Returns the map of section keys and renderers used to construct the usage help message.
* The usage help message can be customized by adding, replacing and removing section renderers from this map.
* Sections can be reordered with {@link #setSectionKeys(List) setSectionKeys}.
* Sections that are either not in this map or not in the list returned by {@link #getSectionKeys() getSectionKeys} are omitted.
* @since 3.9
*/
public Map<String, IHelpSectionRenderer> getSectionMap() { return helpSectionRendererMap; }

/**
* Delegates to {@link #printVersionHelp(PrintStream, Help.Ansi)} with the {@linkplain Help.Ansi#AUTO platform default}.
* @param out the printStream to print to
Expand Down Expand Up @@ -3214,6 +3324,18 @@ private static class NoDefaultProvider implements IDefaultValueProvider {
public String defaultValue(ArgSpec argSpec) { throw new UnsupportedOperationException(); }
}

public interface IHelpFactory {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. We need to think of some javadoc, but I can add that later.

Help create(CommandSpec commandSpec, Help.ColorScheme colorScheme);
}

public static class DefaultHelpFactory implements IHelpFactory {

public Help create(CommandSpec commandSpec, Help.ColorScheme colorScheme) {
return new Help(commandSpec, colorScheme);
}

}

/**
* Factory for instantiating classes that are registered declaratively with annotation attributes, like
* {@link Command#subcommands()}, {@link Option#converter()}, {@link Parameters#converter()} and {@link Command#versionProvider()}.
Expand Down Expand Up @@ -7925,6 +8047,18 @@ public static interface IHelpCommandInitializable {
void init(CommandLine helpCommandLine, Help.Ansi ansi, PrintStream out, PrintStream err);
}


public interface IHelpSectionRenderer {

/**
* Renders a section of the usage help, like header heading, header, synopsis heading,
* synopsis, description heading, description, etc.
* @since 3.9
*/
String render(Help help);

}

/**
* A collection of methods and inner classes that provide fine-grained control over the contents and layout of
* the usage help message to display to end users when help is requested or invalid input values were specified.
Expand Down Expand Up @@ -7975,6 +8109,7 @@ public static class Help {
private final ColorScheme colorScheme;
private final Map<String, Help> commands = new LinkedHashMap<String, Help>();
private List<String> aliases = Collections.emptyList();
private IHelpFactory helpFactory;

private IParamLabelRenderer parameterLabelRenderer;

Expand Down Expand Up @@ -8010,7 +8145,8 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) {
this.aliases.add(0, commandSpec.name());
this.colorScheme = Assert.notNull(colorScheme, "colorScheme").applySystemProperties();
parameterLabelRenderer = createDefaultParamLabelRenderer(); // uses help separator

this.helpFactory = commandSpec.commandLine() != null ? commandSpec.commandLine().getHelpFactory() : new DefaultHelpFactory();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the right way to get the helpfactory.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that looks fine. You raise a good point though: the CommandLine class is mostly a facade, and it may be good to move some of this down to the CommandSpec class. I will go ahead with the merge first, and I may make some changes afterwards.


this.addAllSubcommands(commandSpec.subcommands());
}

Expand All @@ -8023,13 +8159,17 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) {
/** Returns the {@code ColorScheme} model that this Help was constructed with.
* @since 3.0 */
public ColorScheme colorScheme() { return colorScheme; }

/** Returns the {@code ColorScheme} model that this Help was constructed with.
* @since 2.9 */
private IHelpFactory getHelpFactory() { return helpFactory; }

/** Option and positional parameter value label renderer used for the synopsis line(s) and the option list.
* By default initialized to the result of {@link #createDefaultParamLabelRenderer()}, which takes a snapshot
* of the {@link ParserSpec#separator()} at construction time. If the separator is modified after Help construction, you
* may need to re-initialize this field by calling {@link #createDefaultParamLabelRenderer()} again. */
public IParamLabelRenderer parameterLabelRenderer() {return parameterLabelRenderer;}

SysLord marked this conversation as resolved.
Show resolved Hide resolved
/** Registers all specified subcommands with this Help.
* @param commands maps the command names to the associated CommandLine object
* @return this Help instance (for method chaining)
Expand Down Expand Up @@ -8071,7 +8211,7 @@ public Help addAllSubcommands(Map<String, CommandLine> commands) {
* @return this Help instance (for method chaining) */
Help addSubcommand(List<String> commandNames, CommandLine commandLine) {
String all = commandNames.toString();
commands.put(all.substring(1, all.length() - 1), new Help(commandLine.commandSpec, colorScheme).withCommandNames(commandNames));
commands.put(all.substring(1, all.length() - 1), getHelpFactory().create(commandLine.commandSpec, colorScheme).withCommandNames(commandNames));
return this;
}

Expand All @@ -8082,7 +8222,8 @@ Help addSubcommand(List<String> commandNames, CommandLine commandLine) {
* @deprecated
*/
@Deprecated public Help addSubcommand(String commandName, Object command) {
commands.put(commandName, new Help(CommandSpec.forAnnotatedObject(command, commandSpec.commandLine().factory)));
commands.put(commandName,
getHelpFactory().create(CommandSpec.forAnnotatedObject(command, commandSpec.commandLine().factory), defaultColorScheme(Ansi.AUTO)));
return this;
}

Expand Down Expand Up @@ -10137,8 +10278,8 @@ public static class OverwrittenOptionException extends ParameterException {
private static final long serialVersionUID = 1338029208271055776L;
private final ArgSpec overwrittenArg;
public OverwrittenOptionException(CommandLine commandLine, ArgSpec overwritten, String msg) {
super(commandLine, msg);
overwrittenArg = overwritten;
super(commandLine, msg);
overwrittenArg = overwritten;
}
/** Returns the {@link ArgSpec} for the option which was being overwritten.
* @since 3.8 */
Expand Down
45 changes: 45 additions & 0 deletions src/test/java/picocli/CommandLineHelpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2785,6 +2785,51 @@ public void testPalette236ColorBackgroundRgb() {
assertEquals("\u001B[48;5;" + num + "mabc\u001B[49m\u001B[0m", Help.Ansi.ON.new Text("@|bg(3;3;3) abc|@").toString());
}

@Test
public void testHelpFactoryIsUsedWhenSet() {
@Command() class TestCommand { }

IHelpFactory helpFactoryWithOverridenHelpMethod = new IHelpFactory() {
public Help create(CommandSpec commandSpec, ColorScheme colorScheme) {
return new Help(commandSpec, colorScheme) {
@Override
public String detailedSynopsis(int synopsisHeadingLength, Comparator<OptionSpec> optionSort, boolean clusterBooleanOptions) {
return "<custom detailed synopsis>";
}
};
}
};
CommandLine commandLineWithCustomHelpFactory = new CommandLine(new TestCommand()).setHelpFactory(helpFactoryWithOverridenHelpMethod);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
commandLineWithCustomHelpFactory.usage(new PrintStream(baos, true));

assertEquals("Usage: <custom detailed synopsis>", baos.toString());
}

@Test
public void testCustomizableHelpSections() {
@Command(header="<header> (%s)", description="<description>") class TestCommand { }
CommandLine commandLineWithCustomHelpSections = new CommandLine(new TestCommand());

IHelpSectionRenderer renderer = new IHelpSectionRenderer() { public String render(Help help) {
return help.header("<custom header param>");
} };
commandLineWithCustomHelpSections.getSectionMap().put("customSectionExtendsHeader", renderer);

commandLineWithCustomHelpSections.setSectionKeys(Arrays.asList(
CommandLine.DESCRIPTION,
CommandLine.SYNOPSIS_HEADING,
"customSectionExtendsHeader"));

ByteArrayOutputStream baos = new ByteArrayOutputStream();
commandLineWithCustomHelpSections.usage(new PrintStream(baos, true));

String expected = String.format("" +
"<description>%n" +
"Usage: <header> (<custom header param>)%n");
assertEquals(expected, baos.toString());
}

@Test
public void testAnsiEnabled() {
assertTrue(Help.Ansi.ON.enabled());
Expand Down