Skip to content

Commit a70f96b

Browse files
authored
Initial implementation (#292)
1 parent 7b9e8e6 commit a70f96b

3 files changed

Lines changed: 111 additions & 5 deletions

File tree

src/main/java/org/apache/commons/cli/HelpFormatter.java

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,12 @@ public static class Builder implements Supplier<HelpFormatter> {
9090
*/
9191
private PrintWriter printStream = createDefaultPrintWriter();
9292

93+
/** The flag to determine if the since values should be dispalyed */
94+
private boolean showSince;
95+
9396
@Override
9497
public HelpFormatter get() {
95-
return new HelpFormatter(deprecatedFormatFunc, printStream);
98+
return new HelpFormatter(deprecatedFormatFunc, printStream, showSince);
9699
}
97100

98101
/**
@@ -127,6 +130,17 @@ public Builder setShowDeprecated(final Function<Option, String> showDeprecatedFu
127130
this.deprecatedFormatFunc = showDeprecatedFunc;
128131
return this;
129132
}
133+
134+
/**
135+
* Sets whether to show the date the option was first added.
136+
* @param showSince if @{code true} the date the options was first added will be shown.
137+
* @return this builder.
138+
* @since 1.9.0
139+
*/
140+
public Builder setShowSince(final boolean showSince) {
141+
this.showSince = showSince;
142+
return this;
143+
}
130144
}
131145

132146
/**
@@ -151,6 +165,15 @@ public int compare(final Option opt1, final Option opt2) {
151165
return opt1.getKey().compareToIgnoreCase(opt2.getKey());
152166
}
153167
}
168+
/** "Options" text for options header */
169+
private static final String HEADER_OPTIONS = "Options";
170+
171+
/** "Since" text for options header */
172+
private static final String HEADER_SINCE = "Since";
173+
174+
/** "Description" test for options header */
175+
private static final String HEADER_DESCRIPTION = "Description";
176+
154177
/** Default number of characters per line */
155178
public static final int DEFAULT_WIDTH = 74;
156179

@@ -285,6 +308,9 @@ public static String getDescription(final Option option) {
285308
*/
286309
private final PrintWriter printWriter;
287310

311+
/** Flag to determine if since field should be displayed */
312+
private final boolean showSince;
313+
288314
/**
289315
* The separator displayed between the long option and its value.
290316
*/
@@ -294,18 +320,19 @@ public static String getDescription(final Option option) {
294320
* Constructs a new instance.
295321
*/
296322
public HelpFormatter() {
297-
this(null, createDefaultPrintWriter());
323+
this(null, createDefaultPrintWriter(), false);
298324
}
299325

300326
/**
301327
* Constructs a new instance.
302328
* @param printStream TODO
303329
*/
304-
private HelpFormatter(final Function<Option, String> deprecatedFormatFunc, final PrintWriter printStream) {
330+
private HelpFormatter(final Function<Option, String> deprecatedFormatFunc, final PrintWriter printStream, final boolean showSince) {
305331
// TODO All other instance HelpFormatter instance variables.
306332
// Make HelpFormatter immutable for 2.0
307333
this.deprecatedFormatFunc = deprecatedFormatFunc;
308334
this.printWriter = printStream;
335+
this.showSince = showSince;
309336
}
310337

311338
/**
@@ -748,6 +775,12 @@ public void printWrapped(final PrintWriter pw, final int width, final String tex
748775
printWrapped(pw, width, 0, text);
749776
}
750777

778+
private int determineMaxSinceLength(final Options options) {
779+
int minLen = HEADER_SINCE.length();
780+
int len = options.getOptions().stream().map(o -> o.getSince() == null ? minLen : o.getSince().length()).max(Integer::compareTo).orElse(minLen);
781+
return len < minLen ? minLen : len;
782+
}
783+
751784
/**
752785
* Renders the specified Options and return the rendered Options in a StringBuffer.
753786
*
@@ -767,6 +800,7 @@ protected StringBuffer renderOptions(final StringBuffer sb, final int width, fin
767800
// the longest opt string this list will be then used to
768801
// sort options ascending
769802
int max = 0;
803+
int maxSince = showSince ? determineMaxSinceLength(options) + leftPad : 0;
770804
final List<StringBuffer> prefixList = new ArrayList<>();
771805
final List<Option> optList = options.helpOptions();
772806
if (getOptionComparator() != null) {
@@ -792,18 +826,32 @@ protected StringBuffer renderOptions(final StringBuffer sb, final int width, fin
792826
optBuf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">");
793827
}
794828
}
829+
795830
prefixList.add(optBuf);
796-
max = Math.max(optBuf.length(), max);
831+
max = Math.max(optBuf.length() + maxSince, max);
797832
}
833+
final int nextLineTabStop = max + descPad;
834+
if (showSince) {
835+
StringBuilder optHeader = new StringBuilder(HEADER_OPTIONS).append(createPadding(max - maxSince - HEADER_OPTIONS.length() + leftPad))
836+
.append(HEADER_SINCE);
837+
optHeader.append(createPadding(max - optHeader.length())).append(lpad).append(HEADER_DESCRIPTION);
838+
renderWrappedText(sb, width, nextLineTabStop, optHeader.toString());
839+
sb.append(getNewLine());
840+
}
841+
798842
int x = 0;
799843
for (final Iterator<Option> it = optList.iterator(); it.hasNext();) {
800844
final Option option = it.next();
801845
final StringBuilder optBuf = new StringBuilder(prefixList.get(x++).toString());
802846
if (optBuf.length() < max) {
847+
optBuf.append(createPadding(max - maxSince - optBuf.length()));
848+
if (showSince) {
849+
optBuf.append(lpad).append(option.getSince() == null ? "-" : option.getSince());
850+
}
803851
optBuf.append(createPadding(max - optBuf.length()));
804852
}
805853
optBuf.append(dpad);
806-
final int nextLineTabStop = max + descPad;
854+
807855
if (deprecatedFormatFunc != null && option.isDeprecated()) {
808856
optBuf.append(deprecatedFormatFunc.apply(option).trim());
809857
} else if (option.getDescription() != null) {

src/main/java/org/apache/commons/cli/Option.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ private static Class<?> toType(final Class<?> type) {
9393
/** Specifies whether this option is required to be present. */
9494
private boolean required;
9595

96+
/** Specifies the version when this option was added. May be null */
97+
private String since;
98+
9699
/** The type of this Option. */
97100
private Class<?> type = DEFAULT_TYPE;
98101

@@ -282,6 +285,16 @@ public Builder required(final boolean required) {
282285
return this;
283286
}
284287

288+
/** Sets the version number when this option was first defined."
289+
*
290+
* @param since the version number when this option was first defined.
291+
* @return this builder.
292+
*/
293+
public Builder since(final String since) {
294+
this.since = since;
295+
return this;
296+
}
297+
285298
/**
286299
* Sets the type of the Option.
287300
*
@@ -395,6 +408,9 @@ public static Builder builder(final String option) {
395408
/** Specifies whether this option is required to be present. */
396409
private boolean required;
397410

411+
/** Specifies the version when this option was added. May be null */
412+
private String since;
413+
398414
/** The type of this Option. */
399415
private Class<?> type = String.class;
400416

@@ -418,6 +434,7 @@ private Option(final Builder builder) {
418434
this.optionalArg = builder.optionalArg;
419435
this.deprecated = builder.deprecated;
420436
this.required = builder.required;
437+
this.since = builder.since;
421438
this.type = builder.type;
422439
this.valuesep = builder.valueSeparator;
423440
this.converter = builder.converter;
@@ -644,6 +661,15 @@ public String getOpt() {
644661
return option;
645662
}
646663

664+
/**
665+
* Gets the version when this option was added.
666+
* @return the version when this option was added, or {@code null} if not set.
667+
*/
668+
public String getSince() {
669+
return since;
670+
}
671+
672+
647673
/**
648674
* Gets the type of this Option.
649675
*

src/test/java/org/apache/commons/cli/HelpFormatterTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ Licensed to the Apache Software Foundation (ASF) under one or more
1717

1818
package org.apache.commons.cli;
1919

20+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2021
import static org.junit.jupiter.api.Assertions.assertEquals;
2122
import static org.junit.jupiter.api.Assertions.assertNull;
2223
import static org.junit.jupiter.api.Assertions.assertThrows;
2324

2425
import java.io.ByteArrayOutputStream;
26+
import java.io.OutputStreamWriter;
2527
import java.io.PrintWriter;
2628
import java.io.StringWriter;
2729
import java.util.ArrayList;
@@ -490,6 +492,22 @@ public void testPrintHelpWithEmptySyntax() {
490492
assertThrows(IllegalArgumentException.class, () -> formatter.printHelp("", new Options(), false), "null command line syntax should be rejected");
491493
}
492494

495+
@Test
496+
public void testPrintHelpWithSince() {
497+
final String [] expected = {"usage: Command syntax", "Header", "Options Since Description",
498+
" -n,--no-since - Description for n", " -W,--with-since 1.19.0 Descripton for W", "footer"};
499+
final Options options = new Options()
500+
.addOption(Option.builder("W").longOpt("with-since").since("1.19.0").desc("Descripton for W").build())
501+
.addOption(Option.builder("n").longOpt("no-since").desc("Description for n").build());
502+
503+
final HelpFormatter formatter = HelpFormatter.builder().setShowSince(true).get();
504+
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
505+
try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(baos))) {
506+
formatter.printHelp(pw, 80, "Command syntax", "Header", options, 2, 5, "footer", false);
507+
}
508+
assertArrayEquals(expected, baos.toString().split(System.lineSeparator()));
509+
}
510+
493511
@Test
494512
public void testPrintOptionGroupUsage() {
495513
final OptionGroup group = new OptionGroup();
@@ -638,6 +656,20 @@ public void testPrintUsage() {
638656
assertEquals("usage: app [-a] [-b] [-c]" + EOL, bytesOut.toString());
639657
}
640658

659+
@Test
660+
public void testRenderSince() {
661+
final String[] expected = {"Options Since Description", " -n,--no-since - Description for n",
662+
" -W,--with-since 1.19.0 Descripton for W"};
663+
final Options options = new Options()
664+
.addOption(Option.builder("W").longOpt("with-since").since("1.19.0").desc("Descripton for W").build())
665+
.addOption(Option.builder("n").longOpt("no-since").desc("Description for n").build());
666+
final HelpFormatter formatter = HelpFormatter.builder().setShowSince(true).get();
667+
668+
final StringBuffer sb = new StringBuffer();
669+
formatter.renderOptions(sb, 50, options, 2, 5);
670+
assertArrayEquals(expected, sb.toString().split(System.lineSeparator()));
671+
}
672+
641673
@Test
642674
public void testRenderWrappedTextMultiLine() {
643675
// multi line text

0 commit comments

Comments
 (0)