Skip to content

Commit

Permalink
[#299][#459] added support for custom option sections in the generate…
Browse files Browse the repository at this point in the history
…d man page
  • Loading branch information
remkop committed Feb 7, 2020
1 parent 08786c8 commit 757f15c
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import picocli.CommandLine.Help.IOptionRenderer;
import picocli.CommandLine.Help.IParamLabelRenderer;
import picocli.CommandLine.Help.IParameterRenderer;
import picocli.CommandLine.Model.ArgGroupSpec;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.IOrdered;
import picocli.CommandLine.Model.OptionSpec;
Expand Down Expand Up @@ -345,51 +346,79 @@ static void genOptions(PrintWriter pw, CommandSpec spec) {
pw.printf("== Options%n");

IOptionRenderer optionRenderer = spec.commandLine().getHelp().createDefaultOptionRenderer();
IParamLabelRenderer valueLabelRenderer = spec.commandLine().getHelp().createDefaultParamLabelRenderer();
IParamLabelRenderer paramLabelRenderer = spec.commandLine().getHelp().createDefaultParamLabelRenderer();
IParameterRenderer parameterRenderer = spec.commandLine().getHelp().createDefaultParameterRenderer();

List<OptionSpec> options = new ArrayList<OptionSpec>(spec.options()); // options are stored in order of declaration
List<ArgGroupSpec> groups = optionListGroups(spec);
for (ArgGroupSpec group : groups) { options.removeAll(group.options()); }

Comparator<OptionSpec> optionSort = spec.usageMessage().sortOptions()
? new SortByShortestOptionNameAlphabetically()
: createOrderComparatorIfNecessary(spec.options());

List<OptionSpec> options = new ArrayList<OptionSpec>(spec.options()); // options are stored in order of declaration
if (optionSort != null) {
Collections.sort(options, optionSort); // default: sort options ABC
}
// List<ArgGroupSpec> groups = optionListGroups();
// for (ArgGroupSpec group : groups) { options.removeAll(group.options()); }
//
// StringBuilder sb = new StringBuilder();
// layout.addOptions(options, valueLabelRenderer);
// sb.append(layout.toString());
//
// Collections.sort(groups, new SortByOrder<ArgGroupSpec>());
// for (ArgGroupSpec group : groups) {
// sb.append(createHeading(group.heading()));
//
// CommandLine.Help.Layout groupLayout = createDefaultLayout();
// groupLayout.addPositionalParameters(group.positionalParameters(), valueLabelRenderer);
// List<OptionSpec> groupOptions = new ArrayList<OptionSpec>(group.options());
// if (optionSort != null) {
// Collections.sort(groupOptions, optionSort);
// }
// groupLayout.addOptions(groupOptions, valueLabelRenderer);
// sb.append(groupLayout);
// }
// return sb.toString();

for (OptionSpec option : options) {
writeOption(pw, optionRenderer, paramLabelRenderer, option);
}

// now create a custom option section for each arg group that has a heading
Collections.sort(groups, new SortByOrder<ArgGroupSpec>());
for (ArgGroupSpec group : groups) {
pw.println();
Text[][] rows = optionRenderer.render(option, valueLabelRenderer, COLOR_SCHEME);
pw.printf("%s::%n", join(", ", rows[0][1], rows[0][3]));
pw.printf(" %s%n", rows[0][4]);
for (int i = 1; i < rows.length; i++) {
pw.printf("+%n%s%n", rows[i][4]);
String heading = makeHeading(group.heading(), "Options Group");
pw.printf("== %s%n", COLOR_SCHEME.text(heading));

for (PositionalParamSpec positional : group.positionalParameters()) {
writePositional(pw, positional, parameterRenderer, paramLabelRenderer);
}
List<OptionSpec> groupOptions = new ArrayList<OptionSpec>(group.options());
if (optionSort != null) {
Collections.sort(groupOptions, optionSort);
}
for (OptionSpec option : groupOptions) {
writeOption(pw, optionRenderer, paramLabelRenderer, option);
}
}

pw.printf("// end::picocli-generated-man-section-options[]%n");
pw.println();
}

/** Returns the list of {@code ArgGroupSpec}s with a non-{@code null} heading. */
private static List<ArgGroupSpec> optionListGroups(CommandSpec commandSpec) {
List<ArgGroupSpec> result = new ArrayList<ArgGroupSpec>();
optionListGroups(commandSpec.argGroups(), result);
return result;
}
private static void optionListGroups(List<ArgGroupSpec> groups, List<ArgGroupSpec> result) {
for (ArgGroupSpec group : groups) {
optionListGroups(group.subgroups(), result);
if (group.heading() != null) { result.add(group); }
}
}

private static void writeOption(PrintWriter pw, IOptionRenderer optionRenderer, IParamLabelRenderer paramLabelRenderer, OptionSpec option) {
pw.println();
Text[][] rows = optionRenderer.render(option, paramLabelRenderer, COLOR_SCHEME);
pw.printf("%s::%n", join(", ", rows[0][1], rows[0][3]));
pw.printf(" %s%n", rows[0][4]);
for (int i = 1; i < rows.length; i++) {
pw.printf("+%n%s%n", rows[i][4]);
}
}

private static void writePositional(PrintWriter pw, PositionalParamSpec positional, IParameterRenderer parameterRenderer, IParamLabelRenderer paramLabelRenderer) {
pw.println();
Text[][] rows = parameterRenderer.render(positional, paramLabelRenderer, COLOR_SCHEME);
pw.printf("%s::%n", join(", ", rows[0][1], rows[0][3]));
pw.printf(" %s%n", rows[0][4]);
for (int i = 1; i < rows.length; i++) {
pw.printf("+%n%s%n", rows[i][4]);
}
}

static void genPositionalArgs(PrintWriter pw, CommandSpec spec) {
if (spec.positionalParameters().isEmpty() && !spec.usageMessage().showAtFileInUsageHelp()) {
return;
Expand All @@ -399,28 +428,26 @@ static void genPositionalArgs(PrintWriter pw, CommandSpec spec) {

IParameterRenderer parameterRenderer = spec.commandLine().getHelp().createDefaultParameterRenderer();
IParamLabelRenderer paramLabelRenderer = spec.commandLine().getHelp().createDefaultParamLabelRenderer();

if (spec.usageMessage().showAtFileInUsageHelp()) {
CommandLine cmd = new CommandLine(spec).setColorScheme(COLOR_SCHEME);
CommandLine.Help help = cmd.getHelp();
writePositional(pw, help.AT_FILE_POSITIONAL_PARAM, parameterRenderer, paramLabelRenderer);
}
for (PositionalParamSpec positional : spec.positionalParameters()) {

// positional parameters that are part of a group
// are shown in the custom option section for that group
List<PositionalParamSpec> positionals = new ArrayList<PositionalParamSpec>(spec.positionalParameters());
List<ArgGroupSpec> groups = optionListGroups(spec);
for (ArgGroupSpec group : groups) { positionals.removeAll(group.positionalParameters()); }

for (PositionalParamSpec positional : positionals) {
writePositional(pw, positional, parameterRenderer, paramLabelRenderer);
}
pw.printf("// end::picocli-generated-man-section-arguments[]%n");
pw.println();
}

private static void writePositional(PrintWriter pw, PositionalParamSpec positional, IParameterRenderer parameterRenderer, IParamLabelRenderer paramLabelRenderer) {
pw.println();
Text[][] rows = parameterRenderer.render(positional, paramLabelRenderer, COLOR_SCHEME);
pw.printf("%s::%n", join(", ", rows[0][1], rows[0][3]));
pw.printf(" %s%n", rows[0][4]);
for (int i = 1; i < rows.length; i++) {
pw.printf("+%n%s%n", rows[i][4]);
}
}

static void genCommands(PrintWriter pw, CommandSpec spec) {
if (spec.subcommands().isEmpty()) {
return;
Expand Down Expand Up @@ -470,9 +497,7 @@ static void genFooter(PrintWriter pw, CommandSpec spec) {
if (spec.usageMessage().footerHeading().length() == 0 || spec.usageMessage().footer().length == 0) {
return;
}
String heading = spec.usageMessage().footerHeading();
if (heading.endsWith("%n")) { heading = heading.substring(0, heading.length() - 2); }
heading = heading.length() == 0 ? "Footer" : heading.replaceAll("%n", " ");
String heading = makeHeading(spec.usageMessage().footerHeading(), "Footer");
pw.printf("// tag::picocli-generated-man-section-footer[]%n");
pw.printf("== %s%n", COLOR_SCHEME.text(heading));
pw.println();
Expand Down Expand Up @@ -502,6 +527,12 @@ static void genFooter(PrintWriter pw, CommandSpec spec) {
pw.println();
}

private static String makeHeading(String heading, String defaultIfEmpty) {
if (heading.endsWith("%n")) { heading = heading.substring(0, heading.length() - 2); }
heading = heading.trim().length() == 0 ? defaultIfEmpty : heading.replaceAll("%n", " ");
return heading;
}

private static Comparator<OptionSpec> createOrderComparatorIfNecessary(List<OptionSpec> options) {
for (OptionSpec option : options) {
if (option.order() != -1/*OptionSpec.DEFAULT_ORDER*/) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package picocli.codegen.docgen.manpage;

import org.junit.Ignore;
import org.junit.Test;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
Expand All @@ -16,7 +16,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import static org.junit.Assert.*;

Expand Down Expand Up @@ -65,12 +64,39 @@ class MyApp {
assertEquals(expected, sw.toString());
}

static class CsvOptions {
@Option(names = {"-e", "--encoding"}, defaultValue = "Shift_JIS", order = 2,
description = "(CSV/TSV-only) Character encoding of the file to import. Default: ${DEFAULT-VALUE}")
Charset charset;

@Option(names = {"-C", "--column"}, order = 3, paramLabel = "<file-column>=<db-column>", required = true,
description = {"(CSV/TSV-only) Key-value pair specifying the column mapping between the import file column name and the destination table column name."})
Map<String, String> columnMapping;

@Option(names = {"-W", "--column-value"}, order = 4, paramLabel = "<db-column>=<value>",
description = {"(CSV/TSV-only) Key-value pair specifying the destination table column name and the value to set it to."})
Map<String, String> columnValues = new LinkedHashMap<String, String>();

@Option(names = {"--indexed"}, order = 5, description = "(CSV/TSV-only) If true, use indexed access in the file, so specify the (1-based) file column index instead of the file column name.")
boolean indexed;

@Option(names = {"--no-header"}, negatable = true, defaultValue = "true",
description = "(CSV/TSV-only) By default, or if `--header` is specified, the first line of the file is a list of the column names. " +
"If `--no-header` is specified, the first line of the file is data (and indexed access is used).")
boolean header;

@Parameters(description = "Extra CSV file.")
File extraFile;

}
enum Format { CSV, TSV }

@Test
public void testImport() throws IOException {

@Command(name = "import", version = {"import 2.3", "ignored line 1", "ignored line 2"},
description = "Imports data from a file into the infra inventory db.",
optionListHeading = "%nOptions%n", parameterListHeading = "Positional Arguments%n",
footerHeading = "%nExample:%n",
footer = {
"# This imports all rows from the IP_Allocation_v1.20.csv file into the `${table.hosts}` table.",
Expand All @@ -92,25 +118,8 @@ class ImportCommand {
@Parameters(description = "The file to import.")
File file;

@Option(names = {"-e", "--encoding"}, defaultValue = "Shift_JIS", order = 2,
description = "Character encoding of the file to import. Default: ${DEFAULT-VALUE}")
Charset charset;

@Option(names = {"-C", "--column"}, order = 3, paramLabel = "<file-column>=<db-column>", required = true,
description = {"Key-value pair specifying the column mapping between the import file column name and the destination table column name."})
Map<String, String> columnMapping;

@Option(names = {"-W", "--column-value"}, order = 4, paramLabel = "<db-column>=<value>",
description = {"Key-value pair specifying the destination table column name and the value to set it to."})
Map<String, String> columnValues = new LinkedHashMap<String, String>();

@Option(names = {"--indexed"}, order = 5, description = "If true, use indexed access in the file, so specify the (1-based) file column index instead of the file column name.")
boolean indexed;

@Option(names = {"--no-header"}, negatable = true, defaultValue = "true",
description = "By default, or if `--header` is specified, the first line of the file is a list of the column names. " +
"If `--no-header` is specified, the first line of the file is data (and indexed access is used).")
boolean header;
@ArgGroup(validate = false, heading = "%nCSV/TSV-only Options%n")
CsvOptions csvOptions;

@Option(names = {"-n", "--dry-run"},
description = "Don't actually add the row(s), just show if they exist and/or will be ignored..")
Expand All @@ -133,7 +142,7 @@ public void setVerbosity(boolean[] verbosity) {
ManPageGenerator.writeSingleManPage(pw, new CommandLine(new ImportCommand()).getCommandSpec());
pw.flush();

String expected = read("/import.manpage.txt");
String expected = read("/import.manpage.txt.adoc");
expected = expected.replace("\r\n", "\n");
expected = expected.replace("\n", System.getProperty("line.separator"));
assertEquals(expected, sw.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import - Imports data from a file into the infra inventory db.
*import* [*-nv*] [*--indexed*] [*--[no-]header*] [*-e*=_<charset>_] [*-o*=_<format>_]
[*-t*=_<tableName>_] *-C*=_<file-column>=<db-column>_
[*-C*=_<file-column>=<db-column>_]... [*-W*=_<db-column>=<value>_]... _<file>_
_<extraFile>_
// end::picocli-generated-man-section-synopsis[]

// tag::picocli-generated-man-section-description[]
Expand All @@ -31,21 +32,9 @@ Imports data from a file into the infra inventory db.
// tag::picocli-generated-man-section-options[]
== Options

*-C*, *--column*=_<file-column>=<db-column>_::
Key-value pair specifying the column mapping between the import file column name and the destination table column name.

*-e*, *--encoding*=_<charset>_::
Character encoding of the file to import. Default: Shift_JIS

*--indexed*::
If true, use indexed access in the file, so specify the (1-based) file column index instead of the file column name.

*-n*, *--dry-run*::
Don't actually add the row(s), just show if they exist and/or will be ignored..

*--[no-]header*::
By default, or if `--header` is specified, the first line of the file is a list of the column names. If `--no-header` is specified, the first line of the file is data (and indexed access is used).

*-o*, *--format*=_<format>_::
File format. Valid values: CSV, TSV. Default: CSV

Expand All @@ -57,8 +46,25 @@ Imports data from a file into the infra inventory db.
+
For example, `-v -v -v` or `-vvv`

== CSV/TSV-only Options

_<extraFile>_::
Extra CSV file.

*-C*, *--column*=_<file-column>=<db-column>_::
(CSV/TSV-only) Key-value pair specifying the column mapping between the import file column name and the destination table column name.

*-e*, *--encoding*=_<charset>_::
(CSV/TSV-only) Character encoding of the file to import. Default: Shift_JIS

*--indexed*::
(CSV/TSV-only) If true, use indexed access in the file, so specify the (1-based) file column index instead of the file column name.

*--[no-]header*::
(CSV/TSV-only) By default, or if `--header` is specified, the first line of the file is a list of the column names. If `--no-header` is specified, the first line of the file is data (and indexed access is used).

*-W*, *--column-value*=_<db-column>=<value>_::
Key-value pair specifying the destination table column name and the value to set it to.
(CSV/TSV-only) Key-value pair specifying the destination table column name and the value to set it to.
// end::picocli-generated-man-section-options[]

// tag::picocli-generated-man-section-arguments[]
Expand Down

0 comments on commit 757f15c

Please sign in to comment.