Skip to content

Commit 1f9ad9d

Browse files
C5280136remkop
authored andcommitted
Add customizable usage message renderers
1 parent 3825a05 commit 1f9ad9d

File tree

2 files changed

+171
-17
lines changed

2 files changed

+171
-17
lines changed

src/main/java/picocli/CommandLine.java

Lines changed: 126 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import java.util.*;
3939
import java.util.concurrent.Callable;
4040
import java.util.regex.Pattern;
41-
41+
import java.util.stream.Collectors;
4242
import picocli.CommandLine.Help.Ansi.IStyle;
4343
import picocli.CommandLine.Help.Ansi.Style;
4444
import picocli.CommandLine.Help.Ansi.Text;
@@ -140,6 +140,23 @@
140140
* </p>
141141
*/
142142
public class CommandLine {
143+
144+
/** Predefined section keys. */
145+
public static final String HEADER_HEADING = "headerHeading";
146+
public static final String HEADER = "header";
147+
public static final String SYNOPSIS_HEADING = "synopsisHeading";
148+
public static final String SYNOPSIS = "synopsis";
149+
public static final String DESCRIPTION_HEADING = "descriptionHeading";
150+
public static final String DESCRIPTION = "description";
151+
public static final String PARAMETER_LIST_HEADING = "parameterListHeading";
152+
public static final String PARAMETER_LIST = "parameterList";
153+
public static final String OPTION_LIST_HEADING = "optionListHeading";
154+
public static final String OPTION_LIST = "optionList";
155+
public static final String COMMAND_LIST_HEADING = "commandListHeading";
156+
public static final String COMMAND_LIST = "commandList";
157+
public static final String FOOTER_HEADING = "footerHeading";
158+
public static final String FOOTER = "footer";
159+
143160
/** This is picocli version {@value}. */
144161
public static final String VERSION = "4.0.0-SNAPSHOT";
145162

@@ -149,6 +166,24 @@ public class CommandLine {
149166
private final IFactory factory;
150167
private IHelpFactory helpFactory;
151168

169+
private List<String> sectionKeys = Collections.unmodifiableList(Arrays.asList(
170+
HEADER_HEADING,
171+
HEADER,
172+
SYNOPSIS_HEADING,
173+
SYNOPSIS,
174+
DESCRIPTION_HEADING,
175+
DESCRIPTION,
176+
PARAMETER_LIST_HEADING,
177+
PARAMETER_LIST,
178+
OPTION_LIST_HEADING,
179+
OPTION_LIST,
180+
COMMAND_LIST_HEADING,
181+
COMMAND_LIST,
182+
FOOTER_HEADING,
183+
FOOTER));
184+
185+
private Map<String, IHelpSectionRenderer> helpSectionRendererMap = createHelpSectionRendererMap();
186+
152187
/**
153188
* 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.
154189
* <p>The specified object may be a {@link CommandSpec CommandSpec} object, or it may be a {@code @Command}-annotated
@@ -1549,23 +1584,85 @@ public String getUsageMessage(Help.Ansi ansi) {
15491584
public String getUsageMessage(Help.ColorScheme colorScheme) {
15501585
return usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), colorScheme)).toString();
15511586
}
1552-
private static StringBuilder usage(StringBuilder sb, Help help) {
1553-
return sb.append(help.headerHeading())
1554-
.append(help.header())
1555-
.append(help.synopsisHeading()) //e.g. Usage:
1556-
.append(help.synopsis(help.synopsisHeadingLength())) //e.g. &lt;main class&gt; [OPTIONS] &lt;command&gt; [COMMAND-OPTIONS] [ARGUMENTS]
1557-
.append(help.descriptionHeading()) //e.g. %nDescription:%n%n
1558-
.append(help.description()) //e.g. {"Converts foos to bars.", "Use options to control conversion mode."}
1559-
.append(help.parameterListHeading()) //e.g. %nPositional parameters:%n%n
1560-
.append(help.parameterList()) //e.g. [FILE...] the files to convert
1561-
.append(help.optionListHeading()) //e.g. %nOptions:%n%n
1562-
.append(help.optionList()) //e.g. -h, --help displays this help and exits
1563-
.append(help.commandListHeading()) //e.g. %nCommands:%n%n
1564-
.append(help.commandList()) //e.g. add adds the frup to the frooble
1565-
.append(help.footerHeading())
1566-
.append(help.footer());
1587+
1588+
private StringBuilder usage(StringBuilder sb, Help help) {
1589+
for (String key : getSectionKeys()) {
1590+
IHelpSectionRenderer renderer = helpSectionRendererMap.get(key);
1591+
if (renderer != null) { sb.append(renderer.render(help)); }
1592+
}
1593+
return sb;
1594+
}
1595+
1596+
/**
1597+
* Returns the section keys in the order that the usage help message should render the sections.
1598+
* This ordering may be modified with {@link #setSectionKeys(List) setSectionKeys}. The default keys are:
1599+
* <pre>
1600+
* "headerHeading",
1601+
* "header",
1602+
* "synopsisHeading",
1603+
* "synopsis",
1604+
* "descriptionHeading",
1605+
* "description",
1606+
* "parameterListHeading",
1607+
* "parameterList",
1608+
* "optionListHeading",
1609+
* "optionList",
1610+
* "commandListHeading",
1611+
* "commandList",
1612+
* "footerHeading",
1613+
* "footer"
1614+
* </pre>
1615+
* @since 3.9
1616+
*/
1617+
public List<String> getSectionKeys() { return sectionKeys; }
1618+
1619+
/**
1620+
* Sets the section keys in the order that the usage help message should render the sections.
1621+
* @see #getSectionKeys
1622+
* @since 3.9
1623+
*/
1624+
public void setSectionKeys(List<String> keys) { sectionKeys = Collections.unmodifiableList(keys); }
1625+
1626+
/** Returns the help section renderers for the predefined section keys. see: {@link #getSectionKeys()} */
1627+
private Map<String, IHelpSectionRenderer> createHelpSectionRendererMap() {
1628+
Map<String, IHelpSectionRenderer> result = new HashMap<String, IHelpSectionRenderer>();
1629+
1630+
result.put(HEADER_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.headerHeading(); } });
1631+
result.put(HEADER, new IHelpSectionRenderer() { public String render(Help help) { return help.header(); } });
1632+
//e.g. Usage:
1633+
result.put(SYNOPSIS_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.synopsisHeading(); } });
1634+
//e.g. &lt;main class&gt; [OPTIONS] &lt;command&gt; [COMMAND-OPTIONS] [ARGUMENTS]
1635+
result.put(SYNOPSIS, new IHelpSectionRenderer() { public String render(Help help) { return help.synopsis(help.synopsisHeadingLength()); } });
1636+
//e.g. %nDescription:%n%n
1637+
result.put(DESCRIPTION_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.descriptionHeading(); } });
1638+
//e.g. {"Converts foos to bars.", "Use options to control conversion mode."}
1639+
result.put(DESCRIPTION, new IHelpSectionRenderer() { public String render(Help help) { return help.description(); } });
1640+
//e.g. %nPositional parameters:%n%n
1641+
result.put(PARAMETER_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.parameterListHeading(); } });
1642+
//e.g. [FILE...] the files to convert
1643+
result.put(PARAMETER_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.parameterList(); } });
1644+
//e.g. %nOptions:%n%n
1645+
result.put(OPTION_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.optionListHeading(); } });
1646+
//e.g. -h, --help displays this help and exits
1647+
result.put(OPTION_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.optionList(); } });
1648+
//e.g. %nCommands:%n%n
1649+
result.put(COMMAND_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.commandListHeading(); } });
1650+
//e.g. add adds the frup to the frooble
1651+
result.put(COMMAND_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.commandList(); } });
1652+
result.put(FOOTER_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.footerHeading(); } });
1653+
result.put(FOOTER, new IHelpSectionRenderer() { public String render(Help help) { return help.footer(); } });
1654+
return result;
15671655
}
15681656

1657+
/**
1658+
* Returns the map of section keys and renderers used to construct the usage help message.
1659+
* The usage help message can be customized by adding, replacing and removing section renderers from this map.
1660+
* Sections can be reordered with {@link #setSectionKeys(List) setSectionKeys}.
1661+
* Sections that are either not in this map or not in the list returned by {@link #getSectionKeys() getSectionKeys} are omitted.
1662+
* @since 3.9
1663+
*/
1664+
public Map<String, IHelpSectionRenderer> getSectionMap() { return helpSectionRendererMap; }
1665+
15691666
/**
15701667
* Delegates to {@link #printVersionHelp(PrintStream, Help.Ansi)} with the {@linkplain Help.Ansi#AUTO platform default}.
15711668
* @param out the printStream to print to
@@ -7996,6 +8093,18 @@ public static interface IHelpCommandInitializable {
79968093
void init(CommandLine helpCommandLine, Help.Ansi ansi, PrintStream out, PrintStream err);
79978094
}
79988095

8096+
8097+
public interface IHelpSectionRenderer {
8098+
8099+
/**
8100+
* Renders a section of the usage help, like header heading, header, synopsis heading,
8101+
* synopsis, description heading, description, etc.
8102+
* @since 3.9
8103+
*/
8104+
String render(Help help);
8105+
8106+
}
8107+
79998108
/**
80008109
* A collection of methods and inner classes that provide fine-grained control over the contents and layout of
80018110
* the usage help message to display to end users when help is requested or invalid input values were specified.
@@ -8100,7 +8209,7 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) {
81008209
* of the {@link ParserSpec#separator()} at construction time. If the separator is modified after Help construction, you
81018210
* may need to re-initialize this field by calling {@link #createDefaultParamLabelRenderer()} again. */
81028211
public IParamLabelRenderer parameterLabelRenderer() {return parameterLabelRenderer;}
8103-
8212+
81048213
/** Registers all specified subcommands with this Help.
81058214
* @param commands maps the command names to the associated CommandLine object
81068215
* @return this Help instance (for method chaining)

src/test/java/picocli/CommandLineHelpTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2784,6 +2784,51 @@ public void testPalette236ColorBackgroundRgb() {
27842784
assertEquals("\u001B[48;5;" + num + "mabc\u001B[49m\u001B[0m", Help.Ansi.ON.new Text("@|bg(3;3;3) abc|@").toString());
27852785
}
27862786

2787+
@Test
2788+
public void testHelpFactoryIsUsedWhenSet() {
2789+
@Command() class TestCommand { }
2790+
2791+
IHelpFactory helpFactoryWithOverridenHelpMethod = new IHelpFactory() {
2792+
public Help create(CommandSpec commandSpec, ColorScheme colorScheme) {
2793+
return new Help(commandSpec, colorScheme) {
2794+
@Override
2795+
public String detailedSynopsis(int synopsisHeadingLength, Comparator<OptionSpec> optionSort, boolean clusterBooleanOptions) {
2796+
return "<custom detailed synopsis>";
2797+
}
2798+
};
2799+
}
2800+
};
2801+
CommandLine commandLineWithCustomHelpFactory = new CommandLine(new TestCommand()).setHelpFactory(helpFactoryWithOverridenHelpMethod);
2802+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
2803+
commandLineWithCustomHelpFactory.usage(new PrintStream(baos, true));
2804+
2805+
assertEquals("Usage: <custom detailed synopsis>", baos.toString());
2806+
}
2807+
2808+
@Test
2809+
public void testCustomizableHelpSections() {
2810+
@Command(header="<header> (%s)", description="<description>") class TestCommand { }
2811+
CommandLine commandLineWithCustomHelpSections = new CommandLine(new TestCommand());
2812+
2813+
IHelpSectionRenderer renderer = new IHelpSectionRenderer() { public String render(Help help) {
2814+
return help.header("<custom header param>");
2815+
} };
2816+
commandLineWithCustomHelpSections.getSectionMap().put("customSectionExtendsHeader", renderer);
2817+
2818+
commandLineWithCustomHelpSections.setSectionKeys(Arrays.asList(
2819+
CommandLine.DESCRIPTION,
2820+
CommandLine.SYNOPSIS_HEADING,
2821+
"customSectionExtendsHeader"));
2822+
2823+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
2824+
commandLineWithCustomHelpSections.usage(new PrintStream(baos, true));
2825+
2826+
String expected = String.format("" +
2827+
"<description>%n" +
2828+
"Usage: <header> (<custom header param>)%n");
2829+
assertEquals(expected, baos.toString());
2830+
}
2831+
27872832
@Test
27882833
public void testAnsiEnabled() {
27892834
assertTrue(Help.Ansi.ON.enabled());

0 commit comments

Comments
 (0)