Introduce BisqHelpFormatter

Prior to this commit, help output for Bisq executables, e.g. Bisq
Desktop itself used JOptSimple's default HelpFormatter implementation,
which creates a quite cramped and hard-to-read output.

This commit introduces a custom HelpFormatter implementation modeled
after bitcoind's own help output. It maximizes readability while making
full use of an 80-character width.
This commit is contained in:
Chris Beams 2018-11-22 20:34:29 +01:00
parent fc0491d8da
commit 2a537a601b
No known key found for this signature in database
GPG key ID: 3D214F8F5BC5ED73
4 changed files with 308 additions and 0 deletions

View file

@ -114,6 +114,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler {
public void execute(String[] args) throws Exception {
OptionParser parser = new OptionParser();
parser.formatHelpWith(new BisqHelpFormatter());
parser.accepts(HELP_KEY, "This help text").forHelp();
this.customizeOptionParsing(parser);

View file

@ -0,0 +1,120 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.app;
import joptsimple.HelpFormatter;
import joptsimple.OptionDescriptor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class BisqHelpFormatter implements HelpFormatter {
public String format(Map<String, ? extends OptionDescriptor> descriptors) {
StringBuilder output = new StringBuilder();
output.append("Options:\n");
output.append("\n");
for (Map.Entry<String, ? extends OptionDescriptor> entry : descriptors.entrySet()) {
String optionName = entry.getKey();
OptionDescriptor optionDesc = entry.getValue();
if (optionDesc.representsNonOptions())
continue;
output.append(String.format("%s\n", formatOptionSyntax(optionName, optionDesc)));
output.append(String.format("%s\n", formatOptionDescription(optionDesc)));
}
return output.toString();
}
private String formatOptionSyntax(String optionName, OptionDescriptor optionDesc) {
StringBuilder result = new StringBuilder(String.format(" --%s", optionName));
if (optionDesc.acceptsArguments())
result.append(String.format("=<%s>", formatArgDescription(optionDesc)));
List<?> defaultValues = optionDesc.defaultValues();
if (defaultValues.size() > 0)
result.append(String.format(" (default: %s)", formatDefaultValues(defaultValues)));
return result.toString();
}
private String formatArgDescription(OptionDescriptor optionDesc) {
String argDescription = optionDesc.argumentDescription();
if (argDescription.length() > 0)
return argDescription;
String typeIndicator = optionDesc.argumentTypeIndicator();
if (typeIndicator == null)
return "value";
try {
Class<?> type = Class.forName(typeIndicator);
return type.isEnum() ?
Arrays.stream(type.getEnumConstants()).map(Object::toString).collect(Collectors.joining("|")) :
typeIndicator.substring(typeIndicator.lastIndexOf('.') + 1);
} catch (ClassNotFoundException ex) {
// typeIndicator is something other than a class name, which can occur
// in certain cases e.g. where OptionParser.withValuesConvertedBy is used.
return typeIndicator;
}
}
private Object formatDefaultValues(List<?> defaultValues) {
return defaultValues.size() == 1 ?
defaultValues.get(0) :
defaultValues.toString();
}
private String formatOptionDescription(OptionDescriptor optionDesc) {
StringBuilder output = new StringBuilder();
String remainder = optionDesc.description().trim();
// Wrap description text at 80 characters with 8 spaces of indentation and a
// maximum of 72 chars of text, wrapping on spaces. Strings longer than 72 chars
// without any spaces (e.g. a URL) are allowed to overflow the 80-char margin.
while (remainder.length() > 72) {
int idxFirstSpace = remainder.indexOf(' ');
int chunkLen = idxFirstSpace == -1 ? remainder.length() : idxFirstSpace > 73 ? idxFirstSpace : 73;
String chunk = remainder.substring(0, chunkLen);
int idxLastSpace = chunk.lastIndexOf(' ');
int idxBreak = idxLastSpace > 0 ? idxLastSpace : chunk.length();
String line = remainder.substring(0, idxBreak);
output.append(formatLine(line));
remainder = remainder.substring(chunk.length() - (chunk.length() - idxBreak)).trim();
}
if (remainder.length() > 0)
output.append(formatLine(remainder));
return output.toString();
}
private String formatLine(String line) {
return String.format(" %s\n", line.trim());
}
}

View file

@ -0,0 +1,134 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.app;
import joptsimple.OptionParser;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
public class BisqHelpFormatterTest {
@Test
public void testHelpFormatter() throws IOException, URISyntaxException {
OptionParser parser = new OptionParser();
parser.formatHelpWith(new BisqHelpFormatter());
parser.accepts("name",
"The name of the Bisq node")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("Bisq");
parser.accepts("another-option",
"This is a long description which will need to break over multiple linessssssssssss such " +
"that no line is longer than 80 characters in the help output.")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("WAT");
parser.accepts("exactly-72-char-description",
"012345678911234567892123456789312345678941234567895123456789612345678971")
.withRequiredArg()
.ofType(String.class);
parser.accepts("exactly-72-char-description-with-spaces",
" 123456789 123456789 123456789 123456789 123456789 123456789 123456789 1")
.withRequiredArg()
.ofType(String.class);
parser.accepts("90-char-description-without-spaces",
"-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789")
.withRequiredArg()
.ofType(String.class);
parser.accepts("90-char-description-with-space-at-char-80",
"-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789")
.withRequiredArg()
.ofType(String.class);
parser.accepts("90-char-description-with-spaces-at-chars-5-and-80",
"-123 56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789 923456789")
.withRequiredArg()
.ofType(String.class);
parser.accepts("90-char-description-with-space-at-char-73",
"-123456789-223456789-323456789-423456789-523456789-623456789-723456789-8 3456789-923456789")
.withRequiredArg()
.ofType(String.class);
parser.accepts("1-char-description-with-only-a-space", " ")
.withRequiredArg()
.ofType(String.class);
parser.accepts("empty-description", "")
.withRequiredArg()
.ofType(String.class);
parser.accepts("no-description")
.withRequiredArg()
.ofType(String.class);
parser.accepts("no-arg", "Some description");
parser.accepts("optional-arg",
"Option description")
.withOptionalArg();
parser.accepts("with-default-value",
"Some option with a default value")
.withRequiredArg()
.ofType(String.class)
.defaultsTo("Wat");
parser.accepts("data-dir",
"Application data directory")
.withRequiredArg()
.ofType(File.class)
.defaultsTo(new File("/Users/cbeams/Library/Applicaton Support/Bisq"));
parser.accepts("enum-opt",
"Some option that accepts an enum value as an argument")
.withRequiredArg()
.ofType(AnEnum.class)
.defaultsTo(AnEnum.foo);
ByteArrayOutputStream actual = new ByteArrayOutputStream();
String expected = new String(Files.readAllBytes(Paths.get(getClass().getResource("cli-output.txt").toURI())));
parser.printHelpOn(new PrintStream(actual));
assertThat(actual.toString(), equalTo(expected));
}
enum AnEnum {foo, bar, baz}
}

View file

@ -0,0 +1,53 @@
Options:
--name=<String> (default: Bisq)
The name of the Bisq node
--another-option=<String> (default: WAT)
This is a long description which will need to break over multiple
linessssssssssss such that no line is longer than 80 characters in the
help output.
--exactly-72-char-description=<String>
012345678911234567892123456789312345678941234567895123456789612345678971
--exactly-72-char-description-with-spaces=<String>
123456789 123456789 123456789 123456789 123456789 123456789 123456789 1
--90-char-description-without-spaces=<String>
-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789-923456789
--90-char-description-with-space-at-char-80=<String>
-123456789-223456789-323456789-423456789-523456789-623456789-723456789-823456789
923456789
--90-char-description-with-spaces-at-chars-5-and-80=<String>
-123
56789-223456789-323456789-423456789-523456789-623456789-723456789-823456789
923456789
--90-char-description-with-space-at-char-73=<String>
-123456789-223456789-323456789-423456789-523456789-623456789-723456789-8
3456789-923456789
--1-char-description-with-only-a-space=<String>
--empty-description=<String>
--no-description=<String>
--no-arg
Some description
--optional-arg=<value>
Option description
--with-default-value=<String> (default: Wat)
Some option with a default value
--data-dir=<File> (default: /Users/cbeams/Library/Applicaton Support/Bisq)
Application data directory
--enum-opt=<foo|bar|baz> (default: foo)
Some option that accepts an enum value as an argument