Skip to content

Commit a574efb

Browse files
C5280136SysLord
authored andcommitted
Add customizable usage message renderers
1 parent cfeb94b commit a574efb

File tree

2 files changed

+173
-19
lines changed

2 files changed

+173
-19
lines changed

src/main/java/picocli/CommandLine.java

Lines changed: 128 additions & 19 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
@@ -1535,23 +1570,85 @@ public String getUsageMessage(Help.Ansi ansi) {
15351570
public String getUsageMessage(Help.ColorScheme colorScheme) {
15361571
return usage(new StringBuilder(), getHelpFactory().create(getCommandSpec(), colorScheme)).toString();
15371572
}
1538-
private static StringBuilder usage(StringBuilder sb, Help help) {
1539-
return sb.append(help.headerHeading())
1540-
.append(help.header())
1541-
.append(help.synopsisHeading()) //e.g. Usage:
1542-
.append(help.synopsis(help.synopsisHeadingLength())) //e.g. &lt;main class&gt; [OPTIONS] &lt;command&gt; [COMMAND-OPTIONS] [ARGUMENTS]
1543-
.append(help.descriptionHeading()) //e.g. %nDescription:%n%n
1544-
.append(help.description()) //e.g. {"Converts foos to bars.", "Use options to control conversion mode."}
1545-
.append(help.parameterListHeading()) //e.g. %nPositional parameters:%n%n
1546-
.append(help.parameterList()) //e.g. [FILE...] the files to convert
1547-
.append(help.optionListHeading()) //e.g. %nOptions:%n%n
1548-
.append(help.optionList()) //e.g. -h, --help displays this help and exits
1549-
.append(help.commandListHeading()) //e.g. %nCommands:%n%n
1550-
.append(help.commandList()) //e.g. add adds the frup to the frooble
1551-
.append(help.footerHeading())
1552-
.append(help.footer());
1573+
1574+
private StringBuilder usage(StringBuilder sb, Help help) {
1575+
for (String key : getSectionKeys()) {
1576+
IHelpSectionRenderer renderer = helpSectionRendererMap.get(key);
1577+
if (renderer != null) { sb.append(renderer.render(help)); }
1578+
}
1579+
return sb;
1580+
}
1581+
1582+
/**
1583+
* Returns the section keys in the order that the usage help message should render the sections.
1584+
* This ordering may be modified with {@link #setSectionKeys(List) setSectionKeys}. The default keys are:
1585+
* <pre>
1586+
* "headerHeading",
1587+
* "header",
1588+
* "synopsisHeading",
1589+
* "synopsis",
1590+
* "descriptionHeading",
1591+
* "description",
1592+
* "parameterListHeading",
1593+
* "parameterList",
1594+
* "optionListHeading",
1595+
* "optionList",
1596+
* "commandListHeading",
1597+
* "commandList",
1598+
* "footerHeading",
1599+
* "footer"
1600+
* </pre>
1601+
* @since 3.9
1602+
*/
1603+
public List<String> getSectionKeys() { return sectionKeys; }
1604+
1605+
/**
1606+
* Sets the section keys in the order that the usage help message should render the sections.
1607+
* @see #getSectionKeys
1608+
* @since 3.9
1609+
*/
1610+
public void setSectionKeys(List<String> keys) { sectionKeys = Collections.unmodifiableList(keys); }
1611+
1612+
/** Returns the help section renderers for the predefined section keys. see: {@link #getSectionKeys()} */
1613+
private Map<String, IHelpSectionRenderer> createHelpSectionRendererMap() {
1614+
Map<String, IHelpSectionRenderer> result = new HashMap<String, IHelpSectionRenderer>();
1615+
1616+
result.put(HEADER_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.headerHeading(); } });
1617+
result.put(HEADER, new IHelpSectionRenderer() { public String render(Help help) { return help.header(); } });
1618+
//e.g. Usage:
1619+
result.put(SYNOPSIS_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.synopsisHeading(); } });
1620+
//e.g. &lt;main class&gt; [OPTIONS] &lt;command&gt; [COMMAND-OPTIONS] [ARGUMENTS]
1621+
result.put(SYNOPSIS, new IHelpSectionRenderer() { public String render(Help help) { return help.synopsis(help.synopsisHeadingLength()); } });
1622+
//e.g. %nDescription:%n%n
1623+
result.put(DESCRIPTION_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.descriptionHeading(); } });
1624+
//e.g. {"Converts foos to bars.", "Use options to control conversion mode."}
1625+
result.put(DESCRIPTION, new IHelpSectionRenderer() { public String render(Help help) { return help.description(); } });
1626+
//e.g. %nPositional parameters:%n%n
1627+
result.put(PARAMETER_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.parameterListHeading(); } });
1628+
//e.g. [FILE...] the files to convert
1629+
result.put(PARAMETER_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.parameterList(); } });
1630+
//e.g. %nOptions:%n%n
1631+
result.put(OPTION_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.optionListHeading(); } });
1632+
//e.g. -h, --help displays this help and exits
1633+
result.put(OPTION_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.optionList(); } });
1634+
//e.g. %nCommands:%n%n
1635+
result.put(COMMAND_LIST_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.commandListHeading(); } });
1636+
//e.g. add adds the frup to the frooble
1637+
result.put(COMMAND_LIST, new IHelpSectionRenderer() { public String render(Help help) { return help.commandList(); } });
1638+
result.put(FOOTER_HEADING, new IHelpSectionRenderer() { public String render(Help help) { return help.footerHeading(); } });
1639+
result.put(FOOTER, new IHelpSectionRenderer() { public String render(Help help) { return help.footer(); } });
1640+
return result;
15531641
}
15541642

1643+
/**
1644+
* Returns the map of section keys and renderers used to construct the usage help message.
1645+
* The usage help message can be customized by adding, replacing and removing section renderers from this map.
1646+
* Sections can be reordered with {@link #setSectionKeys(List) setSectionKeys}.
1647+
* Sections that are either not in this map or not in the list returned by {@link #getSectionKeys() getSectionKeys} are omitted.
1648+
* @since 3.9
1649+
*/
1650+
public Map<String, IHelpSectionRenderer> getSectionMap() { return helpSectionRendererMap; }
1651+
15551652
/**
15561653
* Delegates to {@link #printVersionHelp(PrintStream, Help.Ansi)} with the {@linkplain Help.Ansi#AUTO platform default}.
15571654
* @param out the printStream to print to
@@ -7950,6 +8047,18 @@ public static interface IHelpCommandInitializable {
79508047
void init(CommandLine helpCommandLine, Help.Ansi ansi, PrintStream out, PrintStream err);
79518048
}
79528049

8050+
8051+
public interface IHelpSectionRenderer {
8052+
8053+
/**
8054+
* Renders a section of the usage help, like header heading, header, synopsis heading,
8055+
* synopsis, description heading, description, etc.
8056+
* @since 3.9
8057+
*/
8058+
String render(Help help);
8059+
8060+
}
8061+
79538062
/**
79548063
* A collection of methods and inner classes that provide fine-grained control over the contents and layout of
79558064
* the usage help message to display to end users when help is requested or invalid input values were specified.
@@ -8054,7 +8163,7 @@ public Help(CommandSpec commandSpec, ColorScheme colorScheme) {
80548163
* of the {@link ParserSpec#separator()} at construction time. If the separator is modified after Help construction, you
80558164
* may need to re-initialize this field by calling {@link #createDefaultParamLabelRenderer()} again. */
80568165
public IParamLabelRenderer parameterLabelRenderer() {return parameterLabelRenderer;}
8057-
8166+
80588167
/** Registers all specified subcommands with this Help.
80598168
* @param commands maps the command names to the associated CommandLine object
80608169
* @return this Help instance (for method chaining)
@@ -10162,8 +10271,8 @@ public static class OverwrittenOptionException extends ParameterException {
1016210271
private static final long serialVersionUID = 1338029208271055776L;
1016310272
private final ArgSpec overwrittenArg;
1016410273
public OverwrittenOptionException(CommandLine commandLine, ArgSpec overwritten, String msg) {
10165-
super(commandLine, msg);
10166-
overwrittenArg = overwritten;
10274+
super(commandLine, msg);
10275+
overwrittenArg = overwritten;
1016710276
}
1016810277
/** Returns the {@link ArgSpec} for the option which was being overwritten.
1016910278
* @since 3.8 */

src/test/java/picocli/CommandLineHelpTest.java

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

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

0 commit comments

Comments
 (0)