Skip to content

Commit 35cf8df

Browse files
committed
Replace internal StringBuffer with StringBuilder
1 parent 8d9604d commit 35cf8df

3 files changed

Lines changed: 172 additions & 113 deletions

File tree

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@
6363
<version>2.16.1</version>
6464
<scope>test</scope>
6565
</dependency>
66+
<dependency>
67+
<groupId>org.mockito</groupId>
68+
<artifactId>mockito-core</artifactId>
69+
<version>4.11.0</version>
70+
<scope>test</scope>
71+
</dependency>
6672
</dependencies>
6773
<properties>
6874
<project.build.outputTimestamp>2024-05-23T21:37:00Z</project.build.outputTimestamp>

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

Lines changed: 150 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more
2222
import java.io.PrintWriter;
2323
import java.io.Serializable;
2424
import java.io.StringReader;
25+
import java.io.UncheckedIOException;
2526
import java.util.ArrayList;
2627
import java.util.Arrays;
2728
import java.util.Collection;
@@ -374,7 +375,6 @@ private void appendOptionGroup(final StringBuilder buff, final OptionGroup group
374375
if (!group.isRequired()) {
375376
buff.append("[");
376377
}
377-
378378
final List<Option> optList = new ArrayList<>(group.getOptions());
379379
if (getOptionComparator() != null) {
380380
Collections.sort(optList, getOptionComparator());
@@ -388,12 +388,135 @@ private void appendOptionGroup(final StringBuilder buff, final OptionGroup group
388388
buff.append(" | ");
389389
}
390390
}
391-
392391
if (!group.isRequired()) {
393392
buff.append("]");
394393
}
395394
}
396395

396+
/**
397+
* Renders the specified Options and return the rendered Options in a StringBuffer.
398+
*
399+
* @param sb The StringBuffer to place the rendered Options into.
400+
* @param width The number of characters to display per line
401+
* @param options The command line Options
402+
* @param leftPad the number of characters of padding to be prefixed to each line
403+
* @param descPad the number of characters of padding to be prefixed to each description line
404+
* @return the StringBuffer with the rendered Options contents.
405+
* @throws IOException if an I/O error occurs.
406+
*/
407+
<A extends Appendable> A appendOptions(final A sb, final int width, final Options options, final int leftPad, final int descPad) throws IOException {
408+
final String lpad = createPadding(leftPad);
409+
final String dpad = createPadding(descPad);
410+
// first create list containing only <lpad>-a,--aaa where
411+
// -a is opt and --aaa is long opt; in parallel look for
412+
// the longest opt string this list will be then used to
413+
// sort options ascending
414+
int max = 0;
415+
final int maxSince = showSince ? determineMaxSinceLength(options) + leftPad : 0;
416+
final List<StringBuilder> prefixList = new ArrayList<>();
417+
final List<Option> optList = options.helpOptions();
418+
if (getOptionComparator() != null) {
419+
Collections.sort(optList, getOptionComparator());
420+
}
421+
for (final Option option : optList) {
422+
final StringBuilder optBuf = new StringBuilder();
423+
if (option.getOpt() == null) {
424+
optBuf.append(lpad).append(" ").append(getLongOptPrefix()).append(option.getLongOpt());
425+
} else {
426+
optBuf.append(lpad).append(getOptPrefix()).append(option.getOpt());
427+
if (option.hasLongOpt()) {
428+
optBuf.append(',').append(getLongOptPrefix()).append(option.getLongOpt());
429+
}
430+
}
431+
if (option.hasArg()) {
432+
final String argName = option.getArgName();
433+
if (argName != null && argName.isEmpty()) {
434+
// if the option has a blank argname
435+
optBuf.append(' ');
436+
} else {
437+
optBuf.append(option.hasLongOpt() ? longOptSeparator : " ");
438+
optBuf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">");
439+
}
440+
}
441+
442+
prefixList.add(optBuf);
443+
max = Math.max(optBuf.length() + maxSince, max);
444+
}
445+
final int nextLineTabStop = max + descPad;
446+
if (showSince) {
447+
final StringBuilder optHeader = new StringBuilder(HEADER_OPTIONS).append(createPadding(max - maxSince - HEADER_OPTIONS.length() + leftPad))
448+
.append(HEADER_SINCE);
449+
optHeader.append(createPadding(max - optHeader.length())).append(lpad).append(HEADER_DESCRIPTION);
450+
appendWrappedText(sb, width, nextLineTabStop, optHeader.toString());
451+
sb.append(getNewLine());
452+
}
453+
454+
int x = 0;
455+
for (final Iterator<Option> it = optList.iterator(); it.hasNext();) {
456+
final Option option = it.next();
457+
final StringBuilder optBuf = new StringBuilder(prefixList.get(x++).toString());
458+
if (optBuf.length() < max) {
459+
optBuf.append(createPadding(max - maxSince - optBuf.length()));
460+
if (showSince) {
461+
optBuf.append(lpad).append(option.getSince() == null ? "-" : option.getSince());
462+
}
463+
optBuf.append(createPadding(max - optBuf.length()));
464+
}
465+
optBuf.append(dpad);
466+
467+
if (deprecatedFormatFunc != null && option.isDeprecated()) {
468+
optBuf.append(deprecatedFormatFunc.apply(option).trim());
469+
} else if (option.getDescription() != null) {
470+
optBuf.append(option.getDescription());
471+
}
472+
appendWrappedText(sb, width, nextLineTabStop, optBuf.toString());
473+
if (it.hasNext()) {
474+
sb.append(getNewLine());
475+
}
476+
}
477+
return sb;
478+
}
479+
480+
/**
481+
* Renders the specified text and return the rendered Options in a StringBuffer.
482+
*
483+
* @param <A> The Appendable implementation.
484+
* @param appendable The StringBuffer to place the rendered text into.
485+
* @param width The number of characters to display per line
486+
* @param nextLineTabStop The position on the next line for the first tab.
487+
* @param text The text to be rendered.
488+
* @return the StringBuffer with the rendered Options contents.
489+
* @throws IOException if an I/O error occurs.
490+
*/
491+
<A extends Appendable> A appendWrappedText(final A appendable, final int width, final int nextLineTabStop, final String text) throws IOException {
492+
String render = text;
493+
int nextLineTabStopPos = nextLineTabStop;
494+
int pos = findWrapPos(render, width, 0);
495+
if (pos == -1) {
496+
appendable.append(rtrim(render));
497+
return appendable;
498+
}
499+
appendable.append(rtrim(render.substring(0, pos))).append(getNewLine());
500+
if (nextLineTabStopPos >= width) {
501+
// stops infinite loop happening
502+
nextLineTabStopPos = 1;
503+
}
504+
// all following lines must be padded with nextLineTabStop space characters
505+
final String padding = createPadding(nextLineTabStopPos);
506+
while (true) {
507+
render = padding + render.substring(pos).trim();
508+
pos = findWrapPos(render, width, 0);
509+
if (pos == -1) {
510+
appendable.append(render);
511+
return appendable;
512+
}
513+
if (render.length() > width && pos == nextLineTabStopPos - 1) {
514+
pos = width;
515+
}
516+
appendable.append(rtrim(render.substring(0, pos))).append(getNewLine());
517+
}
518+
}
519+
397520
/**
398521
* Creates a String of padding of length {@code len}.
399522
*
@@ -407,6 +530,12 @@ protected String createPadding(final int len) {
407530
return new String(padding);
408531
}
409532

533+
private int determineMaxSinceLength(final Options options) {
534+
final int minLen = HEADER_SINCE.length();
535+
final int len = options.getOptions().stream().map(o -> o.getSince() == null ? minLen : o.getSince().length()).max(Integer::compareTo).orElse(minLen);
536+
return len < minLen ? minLen : len;
537+
}
538+
410539
/**
411540
* Finds the next text wrap position after {@code startPos} for the text in {@code text} with the column width
412541
* {@code width}. The wrap point is the last position before startPos+width having a whitespace character (space,
@@ -443,7 +572,6 @@ protected int findWrapPos(final String text, final int width, final int startPos
443572
}
444573
// if we didn't find one, simply chop at startPos+width
445574
pos = startPos + width;
446-
447575
return pos == text.length() ? -1 : pos;
448576
}
449577

@@ -685,9 +813,12 @@ public void printHelp(final String cmdLineSyntax, final String header, final Opt
685813
* @param descPad the number of characters of padding to be prefixed to each description line
686814
*/
687815
public void printOptions(final PrintWriter pw, final int width, final Options options, final int leftPad, final int descPad) {
688-
final StringBuffer sb = new StringBuffer();
689-
renderOptions(sb, width, options, leftPad, descPad);
690-
pw.println(sb.toString());
816+
try {
817+
pw.println(appendOptions(new StringBuilder(), width, options, leftPad, descPad));
818+
} catch (final IOException e) {
819+
// Cannot happen
820+
throw new UncheckedIOException(e);
821+
}
691822
}
692823

693824
/**
@@ -759,9 +890,7 @@ public void printUsage(final PrintWriter pw, final int width, final String app,
759890
* @param text The text to be written to the PrintWriter
760891
*/
761892
public void printWrapped(final PrintWriter pw, final int width, final int nextLineTabStop, final String text) {
762-
final StringBuffer sb = new StringBuffer(text.length());
763-
renderWrappedTextBlock(sb, width, nextLineTabStop, text);
764-
pw.println(sb.toString());
893+
pw.println(renderWrappedTextBlock(new StringBuilder(text.length()), width, nextLineTabStop, text));
765894
}
766895

767896
/**
@@ -775,12 +904,6 @@ public void printWrapped(final PrintWriter pw, final int width, final String tex
775904
printWrapped(pw, width, 0, text);
776905
}
777906

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-
784907
/**
785908
* Renders the specified Options and return the rendered Options in a StringBuffer.
786909
*
@@ -793,76 +916,12 @@ private int determineMaxSinceLength(final Options options) {
793916
* @return the StringBuffer with the rendered Options contents.
794917
*/
795918
protected StringBuffer renderOptions(final StringBuffer sb, final int width, final Options options, final int leftPad, final int descPad) {
796-
final String lpad = createPadding(leftPad);
797-
final String dpad = createPadding(descPad);
798-
// first create list containing only <lpad>-a,--aaa where
799-
// -a is opt and --aaa is long opt; in parallel look for
800-
// the longest opt string this list will be then used to
801-
// sort options ascending
802-
int max = 0;
803-
int maxSince = showSince ? determineMaxSinceLength(options) + leftPad : 0;
804-
final List<StringBuilder> prefixList = new ArrayList<>();
805-
final List<Option> optList = options.helpOptions();
806-
if (getOptionComparator() != null) {
807-
Collections.sort(optList, getOptionComparator());
808-
}
809-
for (final Option option : optList) {
810-
final StringBuilder optBuf = new StringBuilder();
811-
if (option.getOpt() == null) {
812-
optBuf.append(lpad).append(" ").append(getLongOptPrefix()).append(option.getLongOpt());
813-
} else {
814-
optBuf.append(lpad).append(getOptPrefix()).append(option.getOpt());
815-
if (option.hasLongOpt()) {
816-
optBuf.append(',').append(getLongOptPrefix()).append(option.getLongOpt());
817-
}
818-
}
819-
if (option.hasArg()) {
820-
final String argName = option.getArgName();
821-
if (argName != null && argName.isEmpty()) {
822-
// if the option has a blank argname
823-
optBuf.append(' ');
824-
} else {
825-
optBuf.append(option.hasLongOpt() ? longOptSeparator : " ");
826-
optBuf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">");
827-
}
828-
}
829-
830-
prefixList.add(optBuf);
831-
max = Math.max(optBuf.length() + maxSince, max);
832-
}
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-
842-
int x = 0;
843-
for (final Iterator<Option> it = optList.iterator(); it.hasNext();) {
844-
final Option option = it.next();
845-
final StringBuilder optBuf = new StringBuilder(prefixList.get(x++).toString());
846-
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-
}
851-
optBuf.append(createPadding(max - optBuf.length()));
852-
}
853-
optBuf.append(dpad);
854-
855-
if (deprecatedFormatFunc != null && option.isDeprecated()) {
856-
optBuf.append(deprecatedFormatFunc.apply(option).trim());
857-
} else if (option.getDescription() != null) {
858-
optBuf.append(option.getDescription());
859-
}
860-
renderWrappedText(sb, width, nextLineTabStop, optBuf.toString());
861-
if (it.hasNext()) {
862-
sb.append(getNewLine());
863-
}
919+
try {
920+
return appendOptions(sb, width, options, leftPad, descPad);
921+
} catch (final IOException e) {
922+
// Cannot happen
923+
throw new UncheckedIOException(e);
864924
}
865-
return sb;
866925
}
867926

868927
/**
@@ -876,31 +935,11 @@ protected StringBuffer renderOptions(final StringBuffer sb, final int width, fin
876935
* @return the StringBuffer with the rendered Options contents.
877936
*/
878937
protected StringBuffer renderWrappedText(final StringBuffer sb, final int width, final int nextLineTabStop, final String text) {
879-
String render = text;
880-
int nextLineTabStopPos = nextLineTabStop;
881-
int pos = findWrapPos(render, width, 0);
882-
if (pos == -1) {
883-
sb.append(rtrim(render));
884-
return sb;
885-
}
886-
sb.append(rtrim(render.substring(0, pos))).append(getNewLine());
887-
if (nextLineTabStopPos >= width) {
888-
// stops infinite loop happening
889-
nextLineTabStopPos = 1;
890-
}
891-
// all following lines must be padded with nextLineTabStop space characters
892-
final String padding = createPadding(nextLineTabStopPos);
893-
while (true) {
894-
render = padding + render.substring(pos).trim();
895-
pos = findWrapPos(render, width, 0);
896-
if (pos == -1) {
897-
sb.append(render);
898-
return sb;
899-
}
900-
if (render.length() > width && pos == nextLineTabStopPos - 1) {
901-
pos = width;
902-
}
903-
sb.append(rtrim(render.substring(0, pos))).append(getNewLine());
938+
try {
939+
return appendWrappedText(sb, width, nextLineTabStop, text);
940+
} catch (final IOException e) {
941+
// Cannot happen.
942+
throw new UncheckedIOException(e);
904943
}
905944
}
906945

@@ -913,7 +952,7 @@ protected StringBuffer renderWrappedText(final StringBuffer sb, final int width,
913952
* @param nextLineTabStop The position on the next line for the first tab.
914953
* @param text The text to be rendered.
915954
*/
916-
private Appendable renderWrappedTextBlock(final StringBuffer sb, final int width, final int nextLineTabStop, final String text) {
955+
private <A extends Appendable> A renderWrappedTextBlock(final A sb, final int width, final int nextLineTabStop, final String text) {
917956
try {
918957
final BufferedReader in = new BufferedReader(new StringReader(text));
919958
String line;
@@ -924,7 +963,7 @@ private Appendable renderWrappedTextBlock(final StringBuffer sb, final int width
924963
} else {
925964
firstLine = false;
926965
}
927-
renderWrappedText(sb, width, nextLineTabStop, line);
966+
appendWrappedText(sb, width, nextLineTabStop, line);
928967
}
929968
} catch (final IOException e) { // NOPMD
930969
// cannot happen

0 commit comments

Comments
 (0)