You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rp...@apache.org on 2017/10/24 04:14:01 UTC
[1/5] logging-log4j2 git commit: LOG4J2-2088 Upgrade picocli to 2.0
from 0.9.8
Repository: logging-log4j2
Updated Branches:
refs/heads/master 73efe3dcf -> 828797643
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CustomLayoutDemo.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CustomLayoutDemo.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CustomLayoutDemo.java
index 45de326..fe87b97 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CustomLayoutDemo.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CustomLayoutDemo.java
@@ -23,7 +23,6 @@ import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.IParameterRe
import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Layout;
import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.TextTable;
-import java.awt.Point;
import java.lang.reflect.Field;
import static org.apache.logging.log4j.core.tools.picocli.CommandLine.*;
@@ -131,7 +130,7 @@ public class CustomLayoutDemo implements Runnable {
}
class TwoOptionsPerRowLayout extends Layout { // define a custom layout
- Point previous = new Point(0, 0);
+ TextTable.Cell previous = new TextTable.Cell(0, 0);
private TwoOptionsPerRowLayout(Help.ColorScheme colorScheme, TextTable textTable,
IOptionRenderer optionRenderer,
@@ -145,11 +144,11 @@ public class CustomLayoutDemo implements Runnable {
// We want to show two options on one row, next to each other,
// unless the first option spanned multiple columns (in which case there are not enough columns left)
- int col = previous.x + 1;
+ int col = previous.column + 1;
if (col == 1 || col + columnValues.length > table.columns.length) { // if true, write into next row
// table also adds an empty row if a text value spanned multiple columns
- if (table.rowCount() == 0 || table.rowCount() == previous.y + 1) { // avoid adding 2 empty rows
+ if (table.rowCount() == 0 || table.rowCount() == previous.row + 1) { // avoid adding 2 empty rows
table.addEmptyRow(); // create the slots to write the text values into
}
col = 0; // we are starting a new row, reset the column to write into
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/Demo.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/Demo.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/Demo.java
index 17af16e..6f593e2 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/Demo.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/Demo.java
@@ -22,10 +22,14 @@ import org.apache.logging.log4j.core.tools.picocli.CommandLine.Parameters;
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Callable;
/**
* Demonstrates picocli subcommands.
@@ -63,6 +67,9 @@ public class Demo implements Runnable {
CommandLine.run(new Demo(), System.err, args);
}
+ @Option(names = {"-a", "--autocomplete"}, description = "Generate sample autocomplete script for git")
+ private boolean autocomplete;
+
@Option(names = {"-1", "--showUsageForSubcommandGitCommit"}, description = "Shows usage help for the git-commit subcommand")
private boolean showUsageForSubcommandGitCommit;
@@ -174,7 +181,8 @@ public class Demo implements Runnable {
!showRgbColorPalette &&
!showUsageForMainCommand &&
!showUsageForSubcommandGitCommit &&
- !showUsageForSubcommandGitStatus) {
+ !showUsageForSubcommandGitStatus &&
+ !autocomplete) {
CommandLine.usage(this, System.err);
return;
}
@@ -379,7 +387,7 @@ public class Demo implements Runnable {
File file;
@Option(names = {"-m", "--message"}, paramLabel = "<msg>",
- description = " Use the given <msg> as the commit message. If multiple -m options" +
+ description = "Use the given <msg> as the commit message. If multiple -m options" +
" are given, their values are concatenated as separate paragraphs.")
List<String> message = new ArrayList<String>();
@@ -567,7 +575,7 @@ public class Demo implements Runnable {
"Record changes to the repository.%n" +
"%n" +
"git-commit [-ap] [--fixup=<commit>] [--squash=<commit>] [-c=<commit>]%n" +
- " [-C=<commit>] [-F=<file>] [-m[=<msg>...]] [<files>...]%n" +
+ " [-C=<commit>] [-F=<file>] [-m=<msg>]... [<files>]...%n" +
"%n" +
"Description:%n" +
"%n" +
@@ -575,7 +583,7 @@ public class Demo implements Runnable {
"message from the user describing the changes.%n" +
"%n" +
"Parameters:%n" +
- " <files> the files to commit%n" +
+ " [<files>]... the files to commit%n" +
"%n" +
"Options:%n" +
" -a, --all Tell the command to automatically stage files%n" +
@@ -602,7 +610,7 @@ public class Demo implements Runnable {
" commit message options (-m/-c/-C/-F).%n" +
" -F, --file=<file> Take the commit message from the given file. Use%n" +
" - to read the message from the standard input.%n" +
- " -m, --message[=<msg>...] Use the given <msg> as the commit message. If%n" +
+ " -m, --message=<msg> Use the given <msg> as the commit message. If%n" +
" multiple -m options are given, their values are%n" +
" concatenated as separate paragraphs.%n";
@@ -611,7 +619,7 @@ public class Demo implements Runnable {
"Record changes to the repository.%n" +
"%n" +
"@|bold git-commit|@ [@|yellow -ap|@] [@|yellow --fixup|@=@|italic <commit>|@] [@|yellow --squash|@=@|italic <commit>|@] [@|yellow -c|@=@|italic <commit>|@]%n" +
- " [@|yellow -C|@=@|italic <commit>|@] [@|yellow -F|@=@|italic <file>|@] [@|yellow -m|@[=@|italic <msg>|@...]] [@|yellow <files>|@...]%n" +
+ " [@|yellow -C|@=@|italic <commit>|@] [@|yellow -F|@=@|italic <file>|@] [@|yellow -m|@=@|italic <msg>|@]... [@|yellow <files>|@]...%n" +
"%n" +
"@|bold,underline Description:|@%n" +
"%n" +
@@ -619,7 +627,7 @@ public class Demo implements Runnable {
"message from the user describing the changes.%n" +
"%n" +
"@|bold,underline Parameters:|@%n" +
- " @|yellow <files>|@ the files to commit%n" +
+ " [@|yellow <files>|@]... the files to commit%n" +
"%n" +
"@|bold,underline Options:|@%n" +
" @|yellow -a|@, @|yellow --all|@ Tell the command to automatically stage files%n" +
@@ -646,7 +654,62 @@ public class Demo implements Runnable {
" commit message options (-m/-c/-C/-F).%n" +
" @|yellow -F|@, @|yellow --file|@=@|italic <file>|@ Take the commit message from the given file. Use%n" +
" - to read the message from the standard input.%n" +
- " @|yellow -m|@, @|yellow --message|@[=@|italic <msg>|@...] Use the given <msg> as the commit message. If%n" +
+ " @|yellow -m|@, @|yellow --message|@=@|italic <msg>|@ Use the given <msg> as the commit message. If%n" +
" multiple -m options are given, their values are%n" +
" concatenated as separate paragraphs.%n";
+
+ static
+ // tag::CheckSum[]
+ @Command(name = "checksum", description = "Prints the checksum (MD5 by default) of a file to STDOUT.")
+ class CheckSum implements Callable<Void> {
+
+ @Parameters(index = "0", description = "The file whose checksum to calculate.")
+ private File file;
+
+ @Option(names = {"-a", "--algorithm"}, description = "MD5, SHA-1, SHA-256, ...")
+ private String algorithm = "MD5";
+
+ @Option(names = {"-h", "--help"}, usageHelp = true, description = "Show this help message and exit.")
+ private boolean helpRequested;
+
+ public static void main(String[] args) throws Exception {
+ // CheckSum implements Callable,
+ // so parsing and error handling can be done in one line of code
+ CommandLine.call(new CheckSum(), System.err, args);
+ }
+
+ @Override
+ public Void call() throws Exception {
+ // business logic: do different things depending on options the user specified
+ if (helpRequested) {
+ CommandLine.usage(this, System.err);
+ return null;
+ }
+ byte[] digest = MessageDigest.getInstance(algorithm).digest(readBytes(file));
+ print(digest, System.out);
+ return null;
+ }
+
+ byte[] readBytes(File f) throws IOException {
+ int pos = 0;
+ int len = 0;
+ byte[] buffer = new byte[(int) f.length()];
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(f);
+ while ((len = fis.read(buffer, pos, buffer.length - pos)) > 0) { pos += len; }
+ } finally {
+ if (fis != null) { fis.close(); }
+ }
+ return buffer;
+ }
+ void print(byte[] digest, PrintStream out) {
+ for (int i = 0; i < digest.length; i++) {
+ if ((digest[i] & 0xFF) < 16) { out.print('0'); }
+ out.print(Integer.toHexString(digest[i] & 0xFF));
+ }
+ out.println();
+ }
+ }
+ // end::CheckSum[]
}
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/src/changes/changes.xml
----------------------------------------------------------------------
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9dbdd75..ce7294b 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,9 @@
- "remove" - Removed
-->
<release version="2.10.0" date="2017-MM-DD" description="GA Release 2.10.0">
+ <action issue="LOG4J2-2088" dev="rpopma" type="update">
+ Upgrade picocli to 2.0 from 0.9.8.
+ </action>
<action issue="LOG4J2-2087" dev="rpopma" type="fix" due-to="Andy Gumbrecht">
Jansi now needs to be enabled explicitly (by setting system property `log4j.skipJansi` to `false`). To avoid causing problems for web applications, Log4j will no longer automatically try to load Jansi without explicit configuration.
</action>
[5/5] logging-log4j2 git commit: LOG4J2-2088 Upgrade picocli to 2.0
from 0.9.8
Posted by rp...@apache.org.
LOG4J2-2088 Upgrade picocli to 2.0 from 0.9.8
Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo
Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/82879764
Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/82879764
Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/82879764
Branch: refs/heads/master
Commit: 828797643b412bf756d80bce3c3159d2ee308112
Parents: 73efe3d
Author: rpopma <rp...@apache.org>
Authored: Tue Oct 24 13:12:47 2017 +0900
Committer: rpopma <rp...@apache.org>
Committed: Tue Oct 24 13:12:47 2017 +0900
----------------------------------------------------------------------
.../log4j/core/tools/picocli/CommandLine.java | 1573 +++++++++++++-----
.../core/tools/picocli/CommandLineHelpTest.java | 913 +++++++++-
.../core/tools/picocli/CommandLineTest.java | 1307 +++++++++++++--
.../core/tools/picocli/CustomLayoutDemo.java | 7 +-
.../logging/log4j/core/tools/picocli/Demo.java | 79 +-
src/changes/changes.xml | 3 +
6 files changed, 3291 insertions(+), 591 deletions(-)
----------------------------------------------------------------------
[3/5] logging-log4j2 git commit: LOG4J2-2088 Upgrade picocli to 2.0
from 0.9.8
Posted by rp...@apache.org.
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineHelpTest.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineHelpTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineHelpTest.java
index b972d1c..d0bb609 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineHelpTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineHelpTest.java
@@ -37,9 +37,14 @@ import java.io.UnsupportedEncodingException;
import java.lang.String;
import java.lang.reflect.Field;
import java.net.InetAddress;
+import java.net.URI;
+import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import static java.lang.String.format;
import static org.junit.Assert.*;
@@ -123,8 +128,8 @@ public class CommandLineHelpTest {
}
String result = usageString(new Params(), Help.Ansi.OFF);
assertEquals(format("" +
- "Usage: <main class> -x[=<array>...]%n" +
- " -x, --array[=<array>...] the array%n" +
+ "Usage: <main class> -x=<array> [-x=<array>]...%n" +
+ " -x, --array=<array> the array%n" +
" Default: [1, 5, 11, 23]%n"), result);
}
@@ -159,21 +164,645 @@ public class CommandLineHelpTest {
public void testUsageParamLabels() throws Exception {
@Command()
class ParamLabels {
- @Option(names = "-f", paramLabel = "FILE", description = "a file") File f;
- @Option(names = "-n", description = "a number") int number;
+ @Option(names = "-P", paramLabel = "KEY=VALUE", type = {String.class, String.class},
+ description = "Project properties (key-value pairs)") Map<String, String> props;
+ @Option(names = "-f", paramLabel = "FILE", description = "files") File[] f;
+ @Option(names = "-n", description = "a number option") int number;
@Parameters(index = "0", paramLabel = "NUM", description = "number param") int n;
- @Parameters(index = "1", description = "the host") InetAddress host;
+ @Parameters(index = "1", description = "the host parameter") InetAddress host;
}
String result = usageString(new ParamLabels(), Help.Ansi.OFF);
assertEquals(format("" +
- "Usage: <main class> [-f=FILE] [-n=<number>] NUM <host>%n" +
+ "Usage: <main class> [-n=<number>] [-f=FILE]... [-P=KEY=VALUE]... NUM <host>%n" +
" NUM number param%n" +
- " host the host%n" +
+ " <host> the host parameter%n" +
+ " -f= FILE files%n" +
+ " -n= <number> a number option%n" +
+ " -P= KEY=VALUE Project properties (key-value pairs)%n",
+ ""), result);
+ }
+
+ @Test
+ public void testUsageParamLabelsWithLongMapOptionName() throws Exception {
+ @Command()
+ class ParamLabels {
+ @Option(names = {"-P", "--properties"},
+ paramLabel = "KEY=VALUE", type = {String.class, String.class},
+ description = "Project properties (key-value pairs)") Map<String, String> props;
+ @Option(names = "-f", paramLabel = "FILE", description = "a file") File f;
+ @Option(names = "-n", description = "a number option") int number;
+ @Parameters(index = "0", paramLabel = "NUM", description = "number param") int n;
+ @Parameters(index = "1", description = "the host parameter") InetAddress host;
+ }
+ String result = usageString(new ParamLabels(), Help.Ansi.OFF);
+ assertEquals(format("" +
+ "Usage: <main class> [-f=FILE] [-n=<number>] [-P=KEY=VALUE]... NUM <host>%n" +
+ " NUM number param%n" +
+ " <host> the host parameter%n" +
" -f= FILE a file%n" +
- " -n= <number> a number%n",
+ " -n= <number> a number option%n" +
+ " -P, --properties=KEY=VALUE Project properties (key-value pairs)%n",
""), result);
}
+ // ---------------
+ @Test
+ public void testUsageVariableArityRequiredShortOptionArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "-a", required = true, paramLabel = "ARG") // default
+ String[] a;
+ @Option(names = "-b", required = true, paramLabel = "ARG", arity = "0..*")
+ List<String> b;
+ @Option(names = "-c", required = true, paramLabel = "ARG", arity = "1..*")
+ String[] c;
+ @Option(names = "-d", required = true, paramLabel = "ARG", arity = "2..*")
+ List<String> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> -a=ARG [-a=ARG]... -b=[ARG]... [-b=[ARG]...]... -c=ARG...%n" +
+ " [-c=ARG...]... -d=ARG ARG... [-d=ARG ARG...]...%n" +
+ " -a= ARG%n" +
+ " -b= [ARG]...%n" +
+ " -c= ARG...%n" +
+ " -d= ARG ARG...%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageVariableArityShortOptionArray() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "-a", paramLabel = "ARG") // default
+ List<String> a;
+ @Option(names = "-b", paramLabel = "ARG", arity = "0..*")
+ String[] b;
+ @Option(names = "-c", paramLabel = "ARG", arity = "1..*")
+ List<String> c;
+ @Option(names = "-d", paramLabel = "ARG", arity = "2..*")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [-a=ARG]... [-b=[ARG]...]... [-c=ARG...]... [-d=ARG%n" +
+ " ARG...]...%n" +
+ " -a= ARG%n" +
+ " -b= [ARG]...%n" +
+ " -c= ARG...%n" +
+ " -d= ARG ARG...%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityRequiredShortOptionArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "-a", required = true, paramLabel = "ARG", arity = "0..1")
+ List<String> a;
+ @Option(names = "-b", required = true, paramLabel = "ARG", arity = "1..2")
+ String[] b;
+ @Option(names = "-c", required = true, paramLabel = "ARG", arity = "1..3")
+ String[] c;
+ @Option(names = "-d", required = true, paramLabel = "ARG", arity = "2..4")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> -a[=ARG] [-a[=ARG]]... -b=ARG [ARG] [-b=ARG [ARG]]...%n" +
+ " -c=ARG [ARG [ARG]] [-c=ARG [ARG [ARG]]]... -d=ARG ARG [ARG%n" +
+ " [ARG]] [-d=ARG ARG [ARG [ARG]]]...%n" +
+ " -a= [ARG]%n" +
+ " -b= ARG [ARG]%n" +
+ " -c= ARG [ARG [ARG]]%n" +
+ " -d= ARG ARG [ARG [ARG]]%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityShortOptionArray() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "-a", paramLabel = "ARG", arity = "0..1")
+ List<String> a;
+ @Option(names = "-b", paramLabel = "ARG", arity = "1..2")
+ String[] b;
+ @Option(names = "-c", paramLabel = "ARG", arity = "1..3")
+ String[] c;
+ @Option(names = "-d", paramLabel = "ARG", arity = "2..4")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [-a[=ARG]]... [-b=ARG [ARG]]... [-c=ARG [ARG [ARG]]]...%n" +
+ " [-d=ARG ARG [ARG [ARG]]]...%n" +
+ " -a= [ARG]%n" +
+ " -b= ARG [ARG]%n" +
+ " -c= ARG [ARG [ARG]]%n" +
+ " -d= ARG ARG [ARG [ARG]]%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityRequiredShortOptionArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "-a", required = true, paramLabel = "ARG") // default
+ String[] a;
+ @Option(names = "-b", required = true, paramLabel = "ARG", arity = "0")
+ String[] b;
+ @Option(names = "-c", required = true, paramLabel = "ARG", arity = "1")
+ String[] c;
+ @Option(names = "-d", required = true, paramLabel = "ARG", arity = "2")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> -b [-b]... -a=ARG [-a=ARG]... -c=ARG [-c=ARG]... -d=ARG ARG%n" +
+ " [-d=ARG ARG]...%n" +
+ " -a= ARG%n" +
+ " -b%n" +
+ " -c= ARG%n" +
+ " -d= ARG ARG%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityShortOptionArray() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "-a", paramLabel = "ARG") // default
+ String[] a;
+ @Option(names = "-b", paramLabel = "ARG", arity = "0")
+ String[] b;
+ @Option(names = "-c", paramLabel = "ARG", arity = "1")
+ String[] c;
+ @Option(names = "-d", paramLabel = "ARG", arity = "2")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [-b]... [-a=ARG]... [-c=ARG]... [-d=ARG ARG]...%n" +
+ " -a= ARG%n" +
+ " -b%n" +
+ " -c= ARG%n" +
+ " -d= ARG ARG%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+ //--------------
+ @Test
+ public void testUsageVariableArityRequiredLongOptionArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "--aa", required = true, paramLabel = "ARG") // default
+ String[] a;
+ @Option(names = "--bb", required = true, paramLabel = "ARG", arity = "0..*")
+ List<String> b;
+ @Option(names = "--cc", required = true, paramLabel = "ARG", arity = "1..*")
+ String[] c;
+ @Option(names = "--dd", required = true, paramLabel = "ARG", arity = "2..*")
+ List<String> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> --aa=ARG [--aa=ARG]... --bb=[ARG]... [--bb=[ARG]...]...%n" +
+ " --cc=ARG... [--cc=ARG...]... --dd=ARG ARG... [--dd=ARG%n" +
+ " ARG...]...%n" +
+ " --aa=ARG%n" +
+ " --bb=[ARG]...%n" +
+ " --cc=ARG...%n" +
+ " --dd=ARG ARG...%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageVariableArityLongOptionArray() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "--aa", paramLabel = "ARG") // default
+ List<String> a;
+ @Option(names = "--bb", paramLabel = "ARG", arity = "0..*")
+ String[] b;
+ @Option(names = "--cc", paramLabel = "ARG", arity = "1..*")
+ List<String> c;
+ @Option(names = "--dd", paramLabel = "ARG", arity = "2..*")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [--aa=ARG]... [--bb=[ARG]...]... [--cc=ARG...]... [--dd=ARG%n" +
+ " ARG...]...%n" +
+ " --aa=ARG%n" +
+ " --bb=[ARG]...%n" +
+ " --cc=ARG...%n" +
+ " --dd=ARG ARG...%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityRequiredLongOptionArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "--aa", required = true, paramLabel = "ARG", arity = "0..1")
+ List<String> a;
+ @Option(names = "--bb", required = true, paramLabel = "ARG", arity = "1..2")
+ String[] b;
+ @Option(names = "--cc", required = true, paramLabel = "ARG", arity = "1..3")
+ String[] c;
+ @Option(names = "--dd", required = true, paramLabel = "ARG", arity = "2..4", description = "foobar")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> --aa[=ARG] [--aa[=ARG]]... --bb=ARG [ARG] [--bb=ARG%n" +
+ " [ARG]]... --cc=ARG [ARG [ARG]] [--cc=ARG [ARG [ARG]]]...%n" +
+ " --dd=ARG ARG [ARG [ARG]] [--dd=ARG ARG [ARG [ARG]]]...%n" +
+ " --aa[=ARG]%n" +
+ " --bb=ARG [ARG]%n" +
+ " --cc=ARG [ARG [ARG]]%n" +
+ " --dd=ARG ARG [ARG [ARG]]%n" +
+ " foobar%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityLongOptionArray() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "--aa", paramLabel = "ARG", arity = "0..1")
+ List<String> a;
+ @Option(names = "--bb", paramLabel = "ARG", arity = "1..2")
+ String[] b;
+ @Option(names = "--cc", paramLabel = "ARG", arity = "1..3")
+ String[] c;
+ @Option(names = "--dd", paramLabel = "ARG", arity = "2..4", description = "foobar")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [--aa[=ARG]]... [--bb=ARG [ARG]]... [--cc=ARG [ARG%n" +
+ " [ARG]]]... [--dd=ARG ARG [ARG [ARG]]]...%n" +
+ " --aa[=ARG]%n" +
+ " --bb=ARG [ARG]%n" +
+ " --cc=ARG [ARG [ARG]]%n" +
+ " --dd=ARG ARG [ARG [ARG]]%n" +
+ " foobar%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityRequiredLongOptionArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "--aa", required = true, paramLabel = "ARG") // default
+ String[] a;
+ @Option(names = "--bb", required = true, paramLabel = "ARG", arity = "0")
+ String[] b;
+ @Option(names = "--cc", required = true, paramLabel = "ARG", arity = "1")
+ String[] c;
+ @Option(names = "--dd", required = true, paramLabel = "ARG", arity = "2")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> --bb [--bb]... --aa=ARG [--aa=ARG]... --cc=ARG%n" +
+ " [--cc=ARG]... --dd=ARG ARG [--dd=ARG ARG]...%n" +
+ " --aa=ARG%n" +
+ " --bb%n" +
+ " --cc=ARG%n" +
+ " --dd=ARG ARG%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityLongOptionArray() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "--aa", paramLabel = "ARG") // default
+ String[] a;
+ @Option(names = "--bb", paramLabel = "ARG", arity = "0")
+ String[] b;
+ @Option(names = "--cc", paramLabel = "ARG", arity = "1")
+ String[] c;
+ @Option(names = "--dd", paramLabel = "ARG", arity = "2")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [--bb]... [--aa=ARG]... [--cc=ARG]... [--dd=ARG ARG]...%n" +
+ " --aa=ARG%n" +
+ " --bb%n" +
+ " --cc=ARG%n" +
+ " --dd=ARG ARG%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ //------------------
+ @Test
+ public void testUsageVariableArityRequiredShortOptionMap() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "-a", required = true, paramLabel = "KEY=VAL") // default
+ Map<String, String> a;
+ @Option(names = "-b", required = true, arity = "0..*")
+ @SuppressWarnings("unchecked")
+ Map b;
+ @Option(names = "-c", required = true, arity = "1..*", type = {String.class, TimeUnit.class})
+ Map<String, TimeUnit> c;
+ @Option(names = "-d", required = true, arity = "2..*", type = {Integer.class, URL.class}, description = "description")
+ Map<Integer, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> -a=KEY=VAL [-a=KEY=VAL]... -b=[<String=String>]... [-b=%n" +
+ " [<String=String>]...]... -c=<String=TimeUnit>...%n" +
+ " [-c=<String=TimeUnit>...]... -d=<Integer=URL>%n" +
+ " <Integer=URL>... [-d=<Integer=URL> <Integer=URL>...]...%n" +
+ " -a= KEY=VAL%n" +
+ " -b= [<String=String>]...%n" +
+ " -c= <String=TimeUnit>...%n" +
+ " -d= <Integer=URL> <Integer=URL>...%n" +
+ " description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageVariableArityOptionMap() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "-a") // default
+ Map<String, String> a;
+ @Option(names = "-b", arity = "0..*", type = {Integer.class, Integer.class})
+ Map<Integer, Integer> b;
+ @Option(names = "-c", paramLabel = "KEY=VALUE", arity = "1..*", type = {String.class, TimeUnit.class})
+ Map<String, TimeUnit> c;
+ @Option(names = "-d", arity = "2..*", type = {String.class, URL.class}, description = "description")
+ Map<String, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [-a=<String=String>]... [-b=[<Integer=Integer>]...]...%n" +
+ " [-c=KEY=VALUE...]... [-d=<String=URL> <String=URL>...]...%n" +
+ " -a= <String=String>%n" +
+ " -b= [<Integer=Integer>]...%n" +
+ " -c= KEY=VALUE...%n" +
+ " -d= <String=URL> <String=URL>...%n" +
+ " description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityRequiredOptionMap() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "-a", required = true, arity = "0..1", description = "a description")
+ Map<String, String> a;
+ @Option(names = "-b", required = true, arity = "1..2", type = {Integer.class, Integer.class}, description = "b description")
+ Map<Integer, Integer> b;
+ @Option(names = "-c", required = true, arity = "1..3", type = {String.class, URL.class}, description = "c description")
+ Map<String, URL> c;
+ @Option(names = "-d", required = true, paramLabel = "K=URL", arity = "2..4", description = "d description")
+ Map<String, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> -a[=<String=String>] [-a[=<String=String>]]...%n" +
+ " -b=<Integer=Integer> [<Integer=Integer>]%n" +
+ " [-b=<Integer=Integer> [<Integer=Integer>]]...%n" +
+ " -c=<String=URL> [<String=URL> [<String=URL>]]%n" +
+ " [-c=<String=URL> [<String=URL> [<String=URL>]]]... -d=K=URL%n" +
+ " K=URL [K=URL [K=URL]] [-d=K=URL K=URL [K=URL [K=URL]]]...%n" +
+ " -a= [<String=String>] a description%n" +
+ " -b= <Integer=Integer> [<Integer=Integer>]%n" +
+ " b description%n" +
+ " -c= <String=URL> [<String=URL> [<String=URL>]]%n" +
+ " c description%n" +
+ " -d= K=URL K=URL [K=URL [K=URL]]%n" +
+ " d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityOptionMap() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "-a", arity = "0..1"/*, type = {UUID.class, URL.class}*/, description = "a description")
+ Map<UUID, URL> a;
+ @Option(names = "-b", arity = "1..2", type = {Long.class, UUID.class}, description = "b description")
+ Map<?, ?> b;
+ @Option(names = "-c", arity = "1..3", type = {Long.class}, description = "c description")
+ Map<?, ?> c;
+ @Option(names = "-d", paramLabel = "K=V", arity = "2..4", description = "d description")
+ Map<?, ?> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [-a[=<UUID=URL>]]... [-b=<Long=UUID> [<Long=UUID>]]...%n" +
+ " [-c=<String=String> [<String=String> [<String=String>]]]...%n" +
+ " [-d=K=V K=V [K=V [K=V]]]...%n" +
+ " -a= [<UUID=URL>] a description%n" +
+ " -b= <Long=UUID> [<Long=UUID>]%n" +
+ " b description%n" +
+ " -c= <String=String> [<String=String> [<String=String>]]%n" +
+ " c description%n" +
+ " -d= K=V K=V [K=V [K=V]] d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityRequiredOptionMap() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Option(names = "-a", required = true, description = "a description")
+ Map<Short, Field> a;
+ @Option(names = "-b", required = true, paramLabel = "KEY=VAL", arity = "0", description = "b description")
+ @SuppressWarnings("unchecked")
+ Map b;
+ @Option(names = "-c", required = true, arity = "1", type = {Long.class, File.class}, description = "c description")
+ Map<Long, File> c;
+ @Option(names = "-d", required = true, arity = "2", type = {URI.class, URL.class}, description = "d description")
+ Map<URI, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> -b [-b]... -a=<Short=Field> [-a=<Short=Field>]...%n" +
+ " -c=<Long=File> [-c=<Long=File>]... -d=<URI=URL> <URI=URL>%n" +
+ " [-d=<URI=URL> <URI=URL>]...%n" +
+ " -a= <Short=Field> a description%n" +
+ " -b b description%n" +
+ " -c= <Long=File> c description%n" +
+ " -d= <URI=URL> <URI=URL> d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityOptionMap() throws UnsupportedEncodingException {
+ class Args {
+ @Option(names = "-a", type = {Short.class, Field.class}, description = "a description")
+ Map<Short, Field> a;
+ @Option(names = "-b", arity = "0", type = {UUID.class, Long.class}, description = "b description")
+ @SuppressWarnings("unchecked")
+ Map b;
+ @Option(names = "-c", arity = "1", description = "c description")
+ Map<Long, File> c;
+ @Option(names = "-d", arity = "2", type = {URI.class, URL.class}, description = "d description")
+ Map<URI, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [-b]... [-a=<Short=Field>]... [-c=<Long=File>]...%n" +
+ " [-d=<URI=URL> <URI=URL>]...%n" +
+ " -a= <Short=Field> a description%n" +
+ " -b b description%n" +
+ " -c= <Long=File> c description%n" +
+ " -d= <URI=URL> <URI=URL> d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+ //--------------
+ @Test
+ public void testUsageVariableArityParametersArray() throws UnsupportedEncodingException {
+ // if option is required at least once and can be specified multiple times:
+ // -f=ARG [-f=ARG]...
+ class Args {
+ @Parameters(paramLabel = "APARAM", description = "APARAM description")
+ String[] a;
+ @Parameters(arity = "0..*", description = "b description")
+ List<String> b;
+ @Parameters(arity = "1..*", description = "c description")
+ String[] c;
+ @Parameters(arity = "2..*", description = "d description")
+ List<String> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [APARAM]... [<b>]... <c>... <d> <d>...%n" +
+ " [APARAM]... APARAM description%n" +
+ " [<b>]... b description%n" +
+ " <c>... c description%n" +
+ " <d> <d>... d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityParameterArray() throws UnsupportedEncodingException {
+ class Args {
+ @Parameters(index = "0", paramLabel = "PARAMA", arity = "0..1", description = "PARAMA description")
+ List<String> a;
+ @Parameters(index = "0", paramLabel = "PARAMB", arity = "1..2", description = "PARAMB description")
+ String[] b;
+ @Parameters(index = "0", paramLabel = "PARAMC", arity = "1..3", description = "PARAMC description")
+ String[] c;
+ @Parameters(index = "0", paramLabel = "PARAMD", arity = "2..4", description = "PARAMD description")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [PARAMA] PARAMB [PARAMB] PARAMC [PARAMC [PARAMC]] PARAMD%n" +
+ " PARAMD [PARAMD [PARAMD]]%n" +
+ " [PARAMA] PARAMA description%n" +
+ " PARAMB [PARAMB] PARAMB description%n" +
+ " PARAMC [PARAMC [PARAMC]]%n" +
+ " PARAMC description%n" +
+ " PARAMD PARAMD [PARAMD [PARAMD]]%n" +
+ " PARAMD description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityParametersArray() throws UnsupportedEncodingException {
+ class Args {
+ @Parameters(description = "a description (default arity)")
+ String[] a;
+ @Parameters(index = "0", arity = "0", description = "b description (arity=0)")
+ String[] b;
+ @Parameters(index = "1", arity = "1", description = "b description (arity=1)")
+ String[] c;
+ @Parameters(index = "2", arity = "2", description = "b description (arity=2)")
+ String[] d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [<a>]... <c> <d> <d>%n" +
+ " b description (arity=0)%n" +
+ " [<a>]... a description (default arity)%n" +
+ " <c> b description (arity=1)%n" +
+ " <d> <d> b description (arity=2)%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageVariableArityParametersMap() throws UnsupportedEncodingException {
+ class Args {
+ @Parameters()
+ Map<String, String> a;
+ @Parameters(arity = "0..*", description = "a description (arity=0..*)")
+ Map<Integer, Integer> b;
+ @Parameters(paramLabel = "KEY=VALUE", arity = "1..*", type = {String.class, TimeUnit.class})
+ Map<String, TimeUnit> c;
+ @Parameters(arity = "2..*", type = {String.class, URL.class}, description = "description")
+ Map<String, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [<String=String>]... [<Integer=Integer>]... KEY=VALUE...%n" +
+ " <String=URL> <String=URL>...%n" +
+ " [<String=String>]...%n" +
+ " [<Integer=Integer>]... a description (arity=0..*)%n" +
+ " KEY=VALUE...%n" +
+ " <String=URL> <String=URL>...%n" +
+ " description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageRangeArityParametersMap() throws UnsupportedEncodingException {
+ class Args {
+ @Parameters(index = "0", arity = "0..1"/*, type = {UUID.class, URL.class}*/, description = "a description")
+ Map<UUID, URL> a;
+ @Parameters(index = "1", arity = "1..2", type = {Long.class, UUID.class}, description = "b description")
+ Map<?, ?> b;
+ @Parameters(index = "2", arity = "1..3", type = {Long.class}, description = "c description")
+ Map<?, ?> c;
+ @Parameters(index = "3", paramLabel = "K=V", arity = "2..4", description = "d description")
+ Map<?, ?> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [<UUID=URL>] <Long=UUID> [<Long=UUID>] <String=String>%n" +
+ " [<String=String> [<String=String>]] K=V K=V [K=V [K=V]]%n" +
+ " [<UUID=URL>] a description%n" +
+ " <Long=UUID> [<Long=UUID>]%n" +
+ " b description%n" +
+ " <String=String> [<String=String> [<String=String>]]%n" +
+ " c description%n" +
+ " K=V K=V [K=V [K=V]] d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+
+ @Test
+ public void testUsageFixedArityParametersMap() throws UnsupportedEncodingException {
+ class Args {
+ @Parameters(type = {Short.class, Field.class}, description = "a description")
+ Map<Short, Field> a;
+ @Parameters(index = "0", arity = "0", type = {UUID.class, Long.class}, description = "b description (arity=0)")
+ @SuppressWarnings("unchecked")
+ Map b;
+ @Parameters(index = "1", arity = "1", description = "c description")
+ Map<Long, File> c;
+ @Parameters(index = "2", arity = "2", type = {URI.class, URL.class}, description = "d description")
+ Map<URI, URL> d;
+ }
+ String expected = String.format("" +
+ "Usage: <main class> [<Short=Field>]... <Long=File> <URI=URL> <URI=URL>%n" +
+ " b description (arity=0)%n" +
+ " [<Short=Field>]... a description%n" +
+ " <Long=File> c description%n" +
+ " <URI=URL> <URI=URL> d description%n");
+ //CommandLine.usage(new Args(), System.out);
+ assertEquals(expected, usageString(new Args(), Help.Ansi.OFF));
+ }
+ //----------
@Test
public void testShortestFirstComparator_sortsShortestFirst() {
String[] values = {"12345", "12", "123", "123456", "1", "", "1234"};
@@ -206,16 +835,16 @@ public class CommandLineHelpTest {
@Test
public void testSortByOptionArityAndNameComparator_sortsByMaxThenMinThenName() throws Exception {
class App {
- @Option(names = {"-t", "--aaaa"}) boolean tImplicitArity0;
- @Option(names = {"-e", "--EEE"}, arity = "1") boolean explicitArity1;
- @Option(names = {"--bbbb", "-k"}) boolean kImplicitArity0;
- @Option(names = {"--AAAA", "-a"}) int aImplicitArity1;
- @Option(names = {"--BBBB", "-b"}) String[] bImplicitArity0_n;
- @Option(names = {"--ZZZZ", "-z"}, arity = "1..3") String[] zExplicitArity1_3;
- @Option(names = {"-f", "--ffff"}) boolean fImplicitArity0;
+ @Option(names = {"-t", "--aaaa"} ) boolean tImplicitArity0;
+ @Option(names = {"-e", "--EEE"}, arity = "1" ) boolean explicitArity1;
+ @Option(names = {"--bbbb", "-k"} ) boolean kImplicitArity0;
+ @Option(names = {"--AAAA", "-a"} ) int aImplicitArity1;
+ @Option(names = {"--BBBB", "-z"} ) String[] zImplicitArity1;
+ @Option(names = {"--ZZZZ", "-b"}, arity = "1..3") String[] bExplicitArity1_3;
+ @Option(names = {"-f", "--ffff"} ) boolean fImplicitArity0;
}
Field[] fields = fields(App.class, "tImplicitArity0", "explicitArity1", "kImplicitArity0",
- "aImplicitArity1", "bImplicitArity0_n", "zExplicitArity1_3", "fImplicitArity0");
+ "aImplicitArity1", "zImplicitArity1", "bExplicitArity1_3", "fImplicitArity0");
Arrays.sort(fields, new Help.SortByOptionArityAndNameAlphabetically());
Field[] expected = fields(App.class,
"fImplicitArity0",
@@ -223,8 +852,8 @@ public class CommandLineHelpTest {
"tImplicitArity0",
"aImplicitArity1",
"explicitArity1",
- "zExplicitArity1_3",
- "bImplicitArity0_n");
+ "zImplicitArity1",
+ "bExplicitArity1_3");
assertArrayEquals(expected, fields);
}
@@ -289,9 +918,9 @@ public class CommandLineHelpTest {
Help.IParamLabelRenderer parameterRenderer = help.createDefaultParamLabelRenderer();
Field field = help.optionFields.get(0);
Text[][] row1 = renderer.render(field.getAnnotation(Option.class), field, parameterRenderer, help.colorScheme);
- assertEquals(2, row1.length);
+ assertEquals(1, row1.length);
assertArrayEquals(Arrays.toString(row1[0]), textArray(help, "", "-L", ",", "---long=<longField>", "long description"), row1[0]);
- assertArrayEquals(Arrays.toString(row1[1]), textArray(help, "", "", "", "", " Default: null"), row1[1]);
+ //assertArrayEquals(Arrays.toString(row1[1]), textArray(help, "", "", "", "", " Default: null"), row1[1]); // #201 don't show null defaults
field = help.optionFields.get(1);
Text[][] row2 = renderer.render(field.getAnnotation(Option.class), field, parameterRenderer, help.colorScheme);
@@ -357,9 +986,9 @@ public class CommandLineHelpTest {
Help.IParamLabelRenderer parameterRenderer = help.createDefaultParamLabelRenderer();
Field field = help.optionFields.get(0);
Text[][] row = renderer.render(field.getAnnotation(Option.class), field, parameterRenderer, help.colorScheme);
- assertEquals(2, row.length);
+ assertEquals(1, row.length);
assertArrayEquals(Arrays.toString(row[0]), textArray(help, " ", "-b", ",", "-a, --alpha=<otherField>", "other"), row[0]);
- assertArrayEquals(Arrays.toString(row[1]), textArray(help, "", "", "", "", " Default: null"), row[1]);
+ // assertArrayEquals(Arrays.toString(row[1]), textArray(help, "", "", "", "", " Default: null"), row[1]); // #201 don't show null defaults
}
@Test
@@ -373,7 +1002,7 @@ public class CommandLineHelpTest {
Field field = help.positionalParametersFields.get(0);
Text[][] row1 = renderer.render(field.getAnnotation(Parameters.class), field, parameterRenderer, help.colorScheme);
assertEquals(1, row1.length);
- assertArrayEquals(Arrays.toString(row1[0]), textArray(help, " ", "", "", "required", "required"), row1[0]);
+ assertArrayEquals(Arrays.toString(row1[0]), textArray(help, " ", "", "", "<required>", "required"), row1[0]);
}
@Test
@@ -388,7 +1017,7 @@ public class CommandLineHelpTest {
Field field = help.positionalParametersFields.get(0);
Text[][] row1 = renderer.render(field.getAnnotation(Parameters.class), field, parameterRenderer, help.colorScheme);
assertEquals(1, row1.length);
- assertArrayEquals(Arrays.toString(row1[0]), textArray(help, "*", "", "", "required", "required"), row1[0]);
+ assertArrayEquals(Arrays.toString(row1[0]), textArray(help, "*", "", "", "<required>", "required"), row1[0]);
}
@Test
@@ -403,7 +1032,7 @@ public class CommandLineHelpTest {
Field field = help.positionalParametersFields.get(0);
Text[][] row1 = renderer.render(field.getAnnotation(Parameters.class), field, parameterRenderer, help.colorScheme);
assertEquals(1, row1.length);
- assertArrayEquals(Arrays.toString(row1[0]), textArray(help, "", "", "", "optional", "optional"), row1[0]);
+ assertArrayEquals(Arrays.toString(row1[0]), textArray(help, "", "", "", "<optional>", "optional"), row1[0]);
}
@Test
@@ -576,7 +1205,7 @@ public class CommandLineHelpTest {
@Parameters File[] files;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [OPTIONS] [<files>...]" + LINESEP, help.synopsis(0));
+ assertEquals("<main class> [OPTIONS] [<files>]..." + LINESEP, help.synopsis(0));
}
@Test
@@ -589,7 +1218,78 @@ public class CommandLineHelpTest {
@Parameters File[] files;
}
Help help = new Help(new App(), Help.defaultColorScheme(Help.Ansi.ON));
- assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [OPTIONS] [@|yellow <files>|@...]" + LINESEP).toString(), help.synopsis(0));
+ assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [OPTIONS] [@|yellow <files>|@]..." + LINESEP).toString(), help.synopsis(0));
+ }
+
+ @Test
+ public void testAbreviatedSynopsis_commandNameCustomizableDeclaratively() throws UnsupportedEncodingException {
+ @CommandLine.Command(abbreviateSynopsis = true, name = "aprogram")
+ class App {
+ @Option(names = {"--verbose", "-v"}) boolean verbose;
+ @Option(names = {"--count", "-c"}) int count;
+ @Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
+ @Parameters File[] files;
+ }
+ String expected = "" +
+ "Usage: aprogram [OPTIONS] [<files>]...%n" +
+ " [<files>]...%n" +
+ " -c, --count=<count>%n" +
+ " -v, --verbose%n";
+ String actual = usageString(new CommandLine(new App()), Help.Ansi.OFF);
+ assertEquals(String.format(expected), actual);
+ }
+
+ @Test
+ public void testAbreviatedSynopsis_commandNameCustomizableProgrammatically() throws UnsupportedEncodingException {
+ @CommandLine.Command(abbreviateSynopsis = true)
+ class App {
+ @Option(names = {"--verbose", "-v"}) boolean verbose;
+ @Option(names = {"--count", "-c"}) int count;
+ @Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
+ @Parameters File[] files;
+ }
+ String expected = "" +
+ "Usage: anotherProgram [OPTIONS] [<files>]...%n" +
+ " [<files>]...%n" +
+ " -c, --count=<count>%n" +
+ " -v, --verbose%n";
+ String actual = usageString(new CommandLine(new App()).setCommandName("anotherProgram"), Help.Ansi.OFF);
+ assertEquals(String.format(expected), actual);
+ }
+
+ @Test
+ public void testSynopsis_commandNameCustomizableDeclaratively() throws UnsupportedEncodingException {
+ @CommandLine.Command(name = "aprogram")
+ class App {
+ @Option(names = {"--verbose", "-v"}) boolean verbose;
+ @Option(names = {"--count", "-c"}) int count;
+ @Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
+ @Parameters File[] files;
+ }
+ String expected = "" +
+ "Usage: aprogram [-v] [-c=<count>] [<files>]...%n" +
+ " [<files>]...%n" +
+ " -c, --count=<count>%n" +
+ " -v, --verbose%n";
+ String actual = usageString(new CommandLine(new App()), Help.Ansi.OFF);
+ assertEquals(String.format(expected), actual);
+ }
+
+ @Test
+ public void testSynopsis_commandNameCustomizableProgrammatically() throws UnsupportedEncodingException {
+ class App {
+ @Option(names = {"--verbose", "-v"}) boolean verbose;
+ @Option(names = {"--count", "-c"}) int count;
+ @Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
+ @Parameters File[] files;
+ }
+ String expected = "" +
+ "Usage: anotherProgram [-v] [-c=<count>] [<files>]...%n" +
+ " [<files>]...%n" +
+ " -c, --count=<count>%n" +
+ " -v, --verbose%n";
+ String actual = usageString(new CommandLine(new App()).setCommandName("anotherProgram"), Help.Ansi.OFF);
+ assertEquals(String.format(expected), actual);
}
@Test
@@ -600,7 +1300,7 @@ public class CommandLineHelpTest {
@Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [-v] [-c=<count> [<count>...]]" + LINESEP, help.synopsis(0));
+ assertEquals("<main class> [-v] [-c=<count>...]" + LINESEP, help.synopsis(0));
}
@Test
@@ -611,7 +1311,7 @@ public class CommandLineHelpTest {
@Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
}
Help help = new Help(new App(), Help.defaultColorScheme(Help.Ansi.ON));
- assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@ [@|italic <count>|@...]]" + LINESEP),
+ assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@...]" + LINESEP),
help.synopsis(0));
}
@@ -689,7 +1389,8 @@ public class CommandLineHelpTest {
@Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [-v] [-c[=<count>...]]" + LINESEP, help.synopsis(0));
+ // NOTE Expected :<main class> [-v] [-c[=<count>]...] but arity=0 for int field is weird anyway...
+ assertEquals("<main class> [-v] [-c=[<count>]...]" + LINESEP, help.synopsis(0));
}
@Test
@@ -700,7 +1401,25 @@ public class CommandLineHelpTest {
@Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [-v] [-c=<count> [<count>...]]" + LINESEP, help.synopsis(0));
+ assertEquals("<main class> [-v] [-c=<count>...]" + LINESEP, help.synopsis(0));
+ }
+
+ @Test
+ public void testSynopsis_withProgrammaticallySetSeparator_withParameters() throws UnsupportedEncodingException {
+ class App {
+ @Option(names = {"--verbose", "-v"}) boolean verbose;
+ @Option(names = {"--count", "-c"}) int count;
+ @Option(names = {"--help", "-h"}, hidden = true) boolean helpRequested;
+ @Parameters File[] files;
+ }
+ CommandLine commandLine = new CommandLine(new App()).setSeparator(":");
+ String actual = usageString(commandLine, Help.Ansi.OFF);
+ String expected = "" +
+ "Usage: <main class> [-v] [-c:<count>] [<files>]...%n" +
+ " [<files>]...%n" +
+ " -c, --count:<count>%n" +
+ " -v, --verbose%n";
+ assertEquals(String.format(expected), actual);
}
@Test
@@ -712,7 +1431,7 @@ public class CommandLineHelpTest {
@Parameters File[] files;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [-v] [-c:<count>] [<files>...]" + LINESEP, help.synopsis(0));
+ assertEquals("<main class> [-v] [-c:<count>] [<files>]..." + LINESEP, help.synopsis(0));
}
@Test
@@ -724,7 +1443,7 @@ public class CommandLineHelpTest {
@Parameters File[] files;
}
Help help = new Help(new App(), Help.defaultColorScheme(Help.Ansi.ON));
- assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@:@|italic <count>|@] [@|yellow <files>|@...]" + LINESEP),
+ assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@:@|italic <count>|@] [@|yellow <files>|@]..." + LINESEP),
help.synopsis(0));
}
@@ -737,7 +1456,7 @@ public class CommandLineHelpTest {
@Parameters(paramLabel = "FILE") File[] files;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [-v] [-c=<count>] [FILE...]" + LINESEP, help.synopsis(0));
+ assertEquals("<main class> [-v] [-c=<count>] [FILE]..." + LINESEP, help.synopsis(0));
}
@Test
@@ -749,7 +1468,7 @@ public class CommandLineHelpTest {
@Parameters(paramLabel = "FILE") File[] files;
}
Help help = new Help(new App(), Help.defaultColorScheme(Help.Ansi.ON));
- assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@] [@|yellow FILE|@...]" + LINESEP),
+ assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@] [@|yellow FILE|@]..." + LINESEP),
help.synopsis(0));
}
@@ -762,7 +1481,7 @@ public class CommandLineHelpTest {
@Parameters(paramLabel = "FILE", arity = "1..*") File[] files;
}
Help help = new Help(new App(), Help.Ansi.OFF);
- assertEquals("<main class> [-v] [-c=<count>] FILE [FILE...]" + LINESEP, help.synopsis(0));
+ assertEquals("<main class> [-v] [-c=<count>] FILE..." + LINESEP, help.synopsis(0));
}
@Test
@@ -774,7 +1493,7 @@ public class CommandLineHelpTest {
@Parameters(paramLabel = "FILE", arity = "1..*") File[] files;
}
Help help = new Help(new App(), Help.Ansi.ON);
- assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@] @|yellow FILE|@ [@|yellow FILE|@...]" + LINESEP),
+ assertEquals(Help.Ansi.ON.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@] @|yellow FILE|@..." + LINESEP),
help.synopsis(0));
}
@@ -857,14 +1576,14 @@ public class CommandLineHelpTest {
String expected = "" +
"Usage: small-test-program [-!?acorv] [--version] [-h <number>] [-i" + LINESEP +
" <includePattern>] [-p <file>|<folder>] [-d <folder>" + LINESEP +
- " [<folder>]]" + LINESEP;
+ " [<folder>]]..." + LINESEP;
assertEquals(expected, help.synopsisHeading() + help.synopsis(help.synopsisHeadingLength()));
help.synopsisHeading = "Usage:%n";
expected = "" +
"Usage:" + LINESEP +
"small-test-program [-!?acorv] [--version] [-h <number>] [-i <includePattern>]" + LINESEP +
- " [-p <file>|<folder>] [-d <folder> [<folder>]]" + LINESEP;
+ " [-p <file>|<folder>] [-d <folder> [<folder>]]..." + LINESEP;
assertEquals(expected, help.synopsisHeading() + help.synopsis(help.synopsisHeadingLength()));
}
@@ -1106,19 +1825,20 @@ public class CommandLineHelpTest {
@Parameters(index = "0", description = "source host") InetAddress host1;
@Parameters(index = "1", description = "source port") int port1;
@Parameters(index = "2", description = "destination host") InetAddress host2;
- @Parameters(index = "3..4", arity = "1..2", description = "destination port range") int[] port2range;
+ @Parameters(index = "3", arity = "1..2", description = "destination port range") int[] port2range;
@Parameters(index = "4..*", description = "files to transfer") String[] files;
@Parameters(hidden = true) String[] all;
}
String actual = usageString(new App(), Help.Ansi.OFF);
String expected = String.format(
"Usage: <main class> <host1> <port1> <host2> <port2range> [<port2range>]%n" +
- " [<files>...]%n" +
- " host1 source host%n" +
- " port1 source port%n" +
- " host2 destination host%n" +
- " port2range destination port range%n" +
- " files files to transfer%n"
+ " [<files>]...%n" +
+ " <host1> source host%n" +
+ " <port1> source port%n" +
+ " <host2> destination host%n" +
+ " <port2range> [<port2range>]%n" +
+ " destination port range%n" +
+ " [<files>]... files to transfer%n"
);
assertEquals(expected, actual);
}
@@ -1479,7 +2199,7 @@ public class CommandLineHelpTest {
@Test
public void testTextAdjacentStyles() {
- assertEquals("\u001B[3m<commit\u001B[23m\u001B[0m\u001B[3m>\u001B[23m\u001B[0m%n\u001B[0m",
+ assertEquals("\u001B[3m<commit\u001B[23m\u001B[0m\u001B[3m>\u001B[23m\u001B[0m%n",
Help.Ansi.ON.new Text("@|italic <commit|@@|italic >|@%n").toString());
}
@@ -1552,14 +2272,14 @@ public class CommandLineHelpTest {
}
Help.Ansi ansi = Help.Ansi.ON;
// default color scheme
- assertEquals(ansi.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@] @|yellow FILE|@ [@|yellow FILE|@...]" + LINESEP),
+ assertEquals(ansi.new Text("@|bold <main class>|@ [@|yellow -v|@] [@|yellow -c|@=@|italic <count>|@] @|yellow FILE|@..." + LINESEP),
new Help(new App(), ansi).synopsis(0));
System.setProperty("picocli.color.commands", "blue");
System.setProperty("picocli.color.options", "green");
System.setProperty("picocli.color.parameters", "cyan");
System.setProperty("picocli.color.optionParams", "magenta");
- assertEquals(ansi.new Text("@|blue <main class>|@ [@|green -v|@] [@|green -c|@=@|magenta <count>|@] @|cyan FILE|@ [@|cyan FILE|@...]" + LINESEP),
+ assertEquals(ansi.new Text("@|blue <main class>|@ [@|green -v|@] [@|green -c|@=@|magenta <count>|@] @|cyan FILE|@..." + LINESEP),
new Help(new App(), ansi).synopsis(0));
}
@@ -1578,14 +2298,14 @@ public class CommandLineHelpTest {
.parameters(Style.reverse)
.optionParams(Style.bg_green);
// default color scheme
- assertEquals(ansi.new Text("@|faint,bg(magenta) <main class>|@ [@|bg(red) -v|@] [@|bg(red) -c|@=@|bg(green) <count>|@] @|reverse FILE|@ [@|reverse FILE|@...]" + LINESEP),
+ assertEquals(ansi.new Text("@|faint,bg(magenta) <main class>|@ [@|bg(red) -v|@] [@|bg(red) -c|@=@|bg(green) <count>|@] @|reverse FILE|@..." + LINESEP),
new Help(new App(), explicit).synopsis(0));
System.setProperty("picocli.color.commands", "blue");
System.setProperty("picocli.color.options", "blink");
System.setProperty("picocli.color.parameters", "red");
System.setProperty("picocli.color.optionParams", "magenta");
- assertEquals(ansi.new Text("@|blue <main class>|@ [@|blink -v|@] [@|blink -c|@=@|magenta <count>|@] @|red FILE|@ [@|red FILE|@...]" + LINESEP),
+ assertEquals(ansi.new Text("@|blue <main class>|@ [@|blink -v|@] [@|blink -c|@=@|magenta <count>|@] @|red FILE|@..." + LINESEP),
new Help(new App(), explicit).synopsis(0));
}
@@ -1631,4 +2351,95 @@ public class CommandLineHelpTest {
"\u001B[34mBuild 12345\u001B[39m\u001B[0m%n" +
"\u001B[31m\u001B[47m(c) 2017\u001B[49m\u001B[39m\u001B[0m%n"), result);
}
+ @Test
+ public void testCommandLine_printVersionInfo_formatsArguments() throws Exception {
+ @Command(version = {"First line %1$s", "Second line %2$s", "Third line %s %s"}) class Versioned {}
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(baos, true, "UTF8");
+ new CommandLine(new Versioned()).printVersionHelp(ps, Help.Ansi.OFF, "VALUE1", "VALUE2", "VALUE3");
+ String result = baos.toString("UTF8");
+ assertEquals(String.format("First line VALUE1%nSecond line VALUE2%nThird line VALUE1 VALUE2%n"), result);
+ }
+
+ @Test
+ public void testCommandLine_printVersionInfo_withMarkupAndParameterContainingMarkup() throws Exception {
+ @Command(version = {
+ "@|yellow Versioned Command 1.0|@",
+ "@|blue Build 12345|@%1$s",
+ "@|red,bg(white) (c) 2017|@%2$s" })
+ class Versioned {}
+ String[] args = {"@|bold VALUE1|@", "@|underline VALUE2|@", "VALUE3"};
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ PrintStream ps = new PrintStream(baos, true, "UTF8");
+ new CommandLine(new Versioned()).printVersionHelp(ps, Help.Ansi.ON, (Object[]) args);
+ String result = baos.toString("UTF8");
+ assertEquals(String.format("" +
+ "\u001B[33mVersioned Command 1.0\u001B[39m\u001B[0m%n" +
+ "\u001B[34mBuild 12345\u001B[39m\u001B[0m\u001B[1mVALUE1\u001B[21m\u001B[0m%n" +
+ "\u001B[31m\u001B[47m(c) 2017\u001B[49m\u001B[39m\u001B[0m\u001B[4mVALUE2\u001B[24m\u001B[0m%n"), result);
+ }
+
+ @Test
+ public void testMapFieldHelp() throws Exception {
+ class App {
+ @Parameters(arity = "2", split = "\\|",
+ paramLabel = "FIXTAG=VALUE",
+ description = "Exactly two lists of vertical bar '|'-separated FIXTAG=VALUE pairs.")
+ Map<Integer,String> message;
+
+ @Option(names = {"-P", "-map"}, split = ",",
+ paramLabel = "TIMEUNIT=VALUE",
+ description = "Any number of TIMEUNIT=VALUE pairs. These may be specified separately (-PTIMEUNIT=VALUE) or as a comma-separated list.")
+ Map<TimeUnit, String> map;
+ }
+ String actual = usageString(new App(), Help.Ansi.OFF);
+ String expected = String.format("" +
+ "Usage: <main class> [-P=TIMEUNIT=VALUE[,TIMEUNIT=VALUE]...]... FIXTAG=VALUE%n" +
+ " [\\|FIXTAG=VALUE]... FIXTAG=VALUE[\\|FIXTAG=VALUE]...%n" +
+ " FIXTAG=VALUE[\\|FIXTAG=VALUE]... FIXTAG=VALUE[\\|FIXTAG=VALUE]...%n" +
+ " Exactly two lists of vertical bar '|'-separated%n" +
+ " FIXTAG=VALUE pairs.%n" +
+ " -P, -map=TIMEUNIT=VALUE[,TIMEUNIT=VALUE]...%n" +
+ " Any number of TIMEUNIT=VALUE pairs. These may be%n" +
+ " specified separately (-PTIMEUNIT=VALUE) or as a%n" +
+ " comma-separated list.%n");
+ assertEquals(expected, actual);
+ }
+ @Test
+ public void testMapFieldTypeInference() throws UnsupportedEncodingException {
+ class App {
+ @Option(names = "-a") Map<Integer, URI> a;
+ @Option(names = "-b") Map<TimeUnit, StringBuilder> b;
+ @SuppressWarnings("unchecked")
+ @Option(names = "-c") Map c;
+ @Option(names = "-d") List<File> d;
+ @Option(names = "-e") Map<? extends Integer, ? super Long> e;
+ @Option(names = "-f", type = {Long.class, Float.class}) Map<? extends Number, ? super Number> f;
+ @SuppressWarnings("unchecked")
+ @Option(names = "-g", type = {TimeUnit.class, Float.class}) Map g;
+ }
+ String actual = usageString(new App(), Help.Ansi.OFF);
+ String expected = String.format("" +
+ "Usage: <main class> [-a=<Integer=URI>]... [-b=<TimeUnit=StringBuilder>]...%n" +
+ " [-c=<String=String>]... [-d=<d>]... [-e=<Integer=Long>]...%n" +
+ " [-f=<Long=Float>]... [-g=<TimeUnit=Float>]...%n" +
+ " -a= <Integer=URI>%n" +
+ " -b= <TimeUnit=StringBuilder>%n" +
+ "%n" +
+ " -c= <String=String>%n" +
+ " -d= <d>%n" +
+ " -e= <Integer=Long>%n" +
+ " -f= <Long=Float>%n" +
+ " -g= <TimeUnit=Float>%n");
+ assertEquals(expected, actual);
+ }
+ @Test
+ public void test200NPEWithEmptyCommandName() throws UnsupportedEncodingException {
+ @Command(name = "") class Args {}
+ String actual = usageString(new Args(), Help.Ansi.OFF);
+ String expected = String.format("" +
+ "Usage: %n" +
+ "");
+ assertEquals(expected, actual);
+ }
}
[4/5] logging-log4j2 git commit: LOG4J2-2088 Upgrade picocli to 2.0
from 0.9.8
Posted by rp...@apache.org.
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
index 7f51af3..ed6cec1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
@@ -16,7 +16,6 @@
*/
package org.apache.logging.log4j.core.tools.picocli;
-import java.awt.Point;
import java.io.File;
import java.io.PrintStream;
import java.lang.annotation.ElementType;
@@ -26,6 +25,9 @@ import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
@@ -49,17 +51,18 @@ import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Queue;
import java.util.Set;
-import java.util.SortedMap;
import java.util.SortedSet;
import java.util.Stack;
-import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
+import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.IStyle;
@@ -78,7 +81,8 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Colum
* </p><h2>Example</h2>
* <pre>import static picocli.CommandLine.*;
*
- * @Command(header = "Encrypt FILE(s), or standard input, to standard output or to the output file.")
+ * @Command(header = "Encrypt FILE(s), or standard input, to standard output or to the output file.",
+ * version = "v1.2.3")
* public class Encrypt {
*
* @Parameters(type = File.class, description = "Any number of input files")
@@ -90,24 +94,26 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Colum
* @Option(names = { "-v", "--verbose"}, description = "Verbosely list files processed")
* private boolean verbose;
*
- * @Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit")
+ * @Option(names = { "-h", "--help", "-?", "-help"}, usageHelp = true, description = "Display this help and exit")
* private boolean help;
+ *
+ * @Option(names = { "-V", "--version"}, versionHelp = true, description = "Display version info and exit")
+ * private boolean versionHelp;
* }
* </pre>
* <p>
* Use {@code CommandLine} to initialize a domain object as follows:
* </p><pre>
* public static void main(String... args) {
+ * Encrypt encrypt = new Encrypt();
* try {
- * Encrypt encrypt = CommandLine.populateCommand(new Encrypt(), args);
- * if (encrypt.help) {
- * CommandLine.usage(encrypt, System.out);
- * } else {
+ * List<CommandLine> parsedCommands = new CommandLine(encrypt).parse(args);
+ * if (!CommandLine.printHelpIfRequested(parsedCommands, System.err, Help.Ansi.AUTO)) {
* runProgram(encrypt);
* }
* } catch (ParameterException ex) { // command line arguments could not be parsed
* System.err.println(ex.getMessage());
- * CommandLine.usage(new Encrypt(), System.err);
+ * ex.getCommandLine().usage(System.err);
* }
* }
* </pre><p>
@@ -124,18 +130,14 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Colum
* -v -ooutfile in1 in2
* -vooutfile in1 in2
* </pre>
- *
- * <p>
- * Copied and modified from <a href="http://github.com/remkop/picocli/">picocli</a>.
- * </p>
- *
- * @since 2.9
*/
public class CommandLine {
/** This is picocli version {@value}. */
- public static final String VERSION = "0.9.8";
+ public static final String VERSION = "2.0.0";
+ private final Tracer tracer = new Tracer();
private final Interpreter interpreter;
+ private String commandName = Help.DEFAULT_COMMAND_NAME;
private boolean overwrittenOptionsAllowed = false;
private boolean unmatchedArgumentsAllowed = false;
private List<String> unmatchedArguments = new ArrayList<String>();
@@ -149,7 +151,7 @@ public class CommandLine {
* When the {@link #parse(String...)} method is called, fields of the specified object that are annotated
* with {@code @Option} or {@code @Parameters} will be initialized based on command line arguments.
* @param command the object to initialize from the command line arguments
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
*/
public CommandLine(Object command) {
interpreter = new Interpreter(command);
@@ -186,6 +188,7 @@ public class CommandLine {
* converters are registered only with the subcommand hierarchy as it existed when the custom type was registered.
* To ensure a custom type converter is available to all subcommands, register the type converter last, after
* adding subcommands.</p>
+ * <p>See also the {@link Command#subcommands()} annotation to register subcommands declaratively.</p>
*
* @param name the string to recognize on the command line as a subcommand
* @param command the object to initialize with command line arguments following the subcommand name.
@@ -193,6 +196,7 @@ public class CommandLine {
* @return this CommandLine object, to allow method chaining
* @see #registerConverter(Class, ITypeConverter)
* @since 0.9.7
+ * @see Command#subcommands()
*/
public CommandLine addSubcommand(String name, Object command) {
CommandLine commandLine = toCommandLine(command);
@@ -218,21 +222,23 @@ public class CommandLine {
return parent;
}
- /**
- * Returns the annotated object that this {@code CommandLine} instance was constructed with.
+ /** Returns the annotated object that this {@code CommandLine} instance was constructed with.
+ * @param <T> the type of the variable that the return value is being assigned to
* @return the annotated object that this {@code CommandLine} instance was constructed with
* @since 0.9.7
*/
- public Object getCommand() {
- return interpreter.command;
+ public <T> T getCommand() {
+ return (T) interpreter.command;
}
/** Returns {@code true} if an option annotated with {@link Option#usageHelp()} was specified on the command line.
- * @return whether the parser encountered an option annotated with {@link Option#usageHelp()} */
+ * @return whether the parser encountered an option annotated with {@link Option#usageHelp()}.
+ * @since 0.9.8 */
public boolean isUsageHelpRequested() { return usageHelpRequested; }
/** Returns {@code true} if an option annotated with {@link Option#versionHelp()} was specified on the command line.
- * @return whether the parser encountered an option annotated with {@link Option#versionHelp()} */
+ * @return whether the parser encountered an option annotated with {@link Option#versionHelp()}.
+ * @since 0.9.8 */
public boolean isVersionHelpRequested() { return versionHelpRequested; }
/** Returns whether options for single-value fields can be specified multiple times on the command line.
@@ -278,9 +284,10 @@ public class CommandLine {
* subcommands and nested sub-subcommands <em>at the moment this method is called</em>. Subcommands added
* later will have the default setting. To ensure a setting is applied to all
* subcommands, call the setter last, after adding subcommands.</p>
- * @param newValue the new setting
+ * @param newValue the new setting. When {@code true}, the last unmatched arguments are available via the {@link #getUnmatchedArguments()} method.
* @return this {@code CommandLine} object, to allow method chaining
* @since 0.9.7
+ * @see #getUnmatchedArguments()
*/
public CommandLine setUnmatchedArgumentsAllowed(boolean newValue) {
this.unmatchedArgumentsAllowed = newValue;
@@ -315,7 +322,7 @@ public class CommandLine {
* @param args the command line arguments to parse
* @param <T> the type of the annotated object
* @return the specified annotated object
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
* @throws ParameterException if the specified command line arguments are invalid
* @since 0.9.7
*/
@@ -325,21 +332,332 @@ public class CommandLine {
return command;
}
- /**
+ /** Parses the specified command line arguments and returns a list of {@code CommandLine} objects representing the
+ * top-level command and any subcommands (if any) that were recognized and initialized during the parsing process.
* <p>
- * Initializes the annotated object that this {@code CommandLine} was constructed with as well as
- * possibly any registered commands, based on the specified command line arguments,
- * and returns a list of all commands and subcommands that were initialized by this method.
+ * If parsing succeeds, the first element in the returned list is always {@code this CommandLine} object. The
+ * returned list may contain more elements if subcommands were {@linkplain #addSubcommand(String, Object) registered}
+ * and these subcommands were initialized by matching command line arguments. If parsing fails, a
+ * {@link ParameterException} is thrown.
* </p>
*
* @param args the command line arguments to parse
- * @return a list with all commands and subcommands initialized by this method
- * @throws ParameterException if the specified command line arguments are invalid
+ * @return a list with the top-level command and any subcommands initialized by this method
+ * @throws ParameterException if the specified command line arguments are invalid; use
+ * {@link ParameterException#getCommandLine()} to get the command or subcommand whose user input was invalid
*/
public List<CommandLine> parse(String... args) {
return interpreter.parse(args);
}
-
+ /**
+ * Represents a function that can process a List of {@code CommandLine} objects resulting from successfully
+ * {@linkplain #parse(String...) parsing} the command line arguments. This is a
+ * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">functional interface</a>
+ * whose functional method is {@link #handleParseResult(List, PrintStream, CommandLine.Help.Ansi)}.
+ * <p>
+ * Implementations of this functions can be passed to the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) CommandLine::parseWithHandler}
+ * methods to take some next step after the command line was successfully parsed.
+ * </p>
+ * @see RunFirst
+ * @see RunLast
+ * @see RunAll
+ * @since 2.0 */
+ public static interface IParseResultHandler {
+ /** Processes a List of {@code CommandLine} objects resulting from successfully
+ * {@linkplain #parse(String...) parsing} the command line arguments and optionally returns a list of results.
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return a list of results, or an empty list if there are no results
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) throws ExecutionException;
+ }
+ /**
+ * Represents a function that can handle a {@code ParameterException} that occurred while
+ * {@linkplain #parse(String...) parsing} the command line arguments. This is a
+ * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">functional interface</a>
+ * whose functional method is {@link #handleException(CommandLine.ParameterException, PrintStream, CommandLine.Help.Ansi, String...)}.
+ * <p>
+ * Implementations of this functions can be passed to the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) CommandLine::parseWithHandler}
+ * methods to handle situations when the command line could not be parsed.
+ * </p>
+ * @see DefaultExceptionHandler
+ * @since 2.0 */
+ public static interface IExceptionHandler {
+ /** Handles a {@code ParameterException} that occurred while {@linkplain #parse(String...) parsing} the command
+ * line arguments and optionally returns a list of results.
+ * @param ex the ParameterException describing the problem that occurred while parsing the command line arguments,
+ * and the CommandLine representing the command or subcommand whose input was invalid
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @param args the command line arguments that could not be parsed
+ * @return a list of results, or an empty list if there are no results
+ */
+ List<Object> handleException(ParameterException ex, PrintStream out, Help.Ansi ansi, String... args);
+ }
+ /**
+ * Default exception handler that prints the exception message to the specified {@code PrintStream}, followed by the
+ * usage message for the command or subcommand whose input was invalid.
+ * <p>Implementation roughly looks like this:</p>
+ * <pre>
+ * System.err.println(paramException.getMessage());
+ * paramException.getCommandLine().usage(System.err);
+ * </pre>
+ * @since 2.0 */
+ public static class DefaultExceptionHandler implements IExceptionHandler {
+ @Override
+ public List<Object> handleException(ParameterException ex, PrintStream out, Help.Ansi ansi, String... args) {
+ out.println(ex.getMessage());
+ ex.getCommandLine().usage(out, ansi);
+ return Collections.emptyList();
+ }
+ }
+ /**
+ * Helper method that may be useful when processing the list of {@code CommandLine} objects that result from successfully
+ * {@linkplain #parse(String...) parsing} command line arguments. This method prints out
+ * {@linkplain #usage(PrintStream, Help.Ansi) usage help} if {@linkplain #isUsageHelpRequested() requested}
+ * or {@linkplain #printVersionHelp(PrintStream, Help.Ansi) version help} if {@linkplain #isVersionHelpRequested() requested}
+ * and returns {@code true}. Otherwise, if none of the specified {@code CommandLine} objects have help requested,
+ * this method returns {@code false}.
+ * <p>
+ * Note that this method <em>only</em> looks at the {@link Option#usageHelp() usageHelp} and
+ * {@link Option#versionHelp() versionHelp} attributes. The {@link Option#help() help} attribute is ignored.
+ * </p>
+ * @param parsedCommands the list of {@code CommandLine} objects to check if help was requested
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return {@code true} if help was printed, {@code false} otherwise
+ * @since 2.0 */
+ public static boolean printHelpIfRequested(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ for (CommandLine parsed : parsedCommands) {
+ if (parsed.isUsageHelpRequested()) {
+ parsed.usage(out, ansi);
+ return true;
+ } else if (parsed.isVersionHelpRequested()) {
+ parsed.printVersionHelp(out, ansi);
+ return true;
+ }
+ }
+ return false;
+ }
+ private static Object execute(CommandLine parsed) {
+ Object command = parsed.getCommand();
+ if (command instanceof Runnable) {
+ try {
+ ((Runnable) command).run();
+ return null;
+ } catch (Exception ex) {
+ throw new ExecutionException(parsed, "Error while running command (" + command + ")", ex);
+ }
+ } else if (command instanceof Callable) {
+ try {
+ return ((Callable<Object>) command).call();
+ } catch (Exception ex) {
+ throw new ExecutionException(parsed, "Error while calling command (" + command + ")", ex);
+ }
+ }
+ throw new ExecutionException(parsed, "Parsed command (" + command + ") is not Runnable or Callable");
+ }
+ /**
+ * Command line parse result handler that prints help if requested, and otherwise executes the top-level
+ * {@code Runnable} or {@code Callable} command.
+ * For use in the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) parseWithHandler} methods.
+ * <p>
+ * From picocli v2.0, {@code RunFirst} is used to implement the {@link #run(Runnable, PrintStream, Help.Ansi, String...) run}
+ * and {@link #call(Callable, PrintStream, Help.Ansi, String...) call} convenience methods.
+ * </p>
+ * @since 2.0 */
+ public static class RunFirst implements IParseResultHandler {
+ /** Prints help if requested, and otherwise executes the top-level {@code Runnable} or {@code Callable} command.
+ * If the top-level command does not implement either {@code Runnable} or {@code Callable}, a {@code ExecutionException}
+ * is thrown detailing the problem and capturing the offending {@code CommandLine} object.
+ *
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return an empty list if help was requested, or a list containing a single element: the result of calling the
+ * {@code Callable}, or a {@code null} element if the top-level command was a {@code Runnable}
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
+ return Arrays.asList(execute(parsedCommands.get(0)));
+ }
+ }
+ /**
+ * Command line parse result handler that prints help if requested, and otherwise executes the most specific
+ * {@code Runnable} or {@code Callable} subcommand.
+ * For use in the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) parseWithHandler} methods.
+ * <p>
+ * Something like this:</p>
+ * <pre>
+ * // RunLast implementation: print help if requested, otherwise execute the most specific subcommand
+ * if (CommandLine.printHelpIfRequested(parsedCommands, System.err, Help.Ansi.AUTO)) {
+ * return emptyList();
+ * }
+ * CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ * Object command = last.getCommand();
+ * if (command instanceof Runnable) {
+ * try {
+ * ((Runnable) command).run();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error in runnable " + command, ex);
+ * }
+ * } else if (command instanceof Callable) {
+ * Object result;
+ * try {
+ * result = ((Callable) command).call();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error in callable " + command, ex);
+ * }
+ * // ...do something with result
+ * } else {
+ * throw new ExecutionException(last, "Parsed command (" + command + ") is not Runnable or Callable");
+ * }
+ * </pre>
+ * @since 2.0 */
+ public static class RunLast implements IParseResultHandler {
+ /** Prints help if requested, and otherwise executes the most specific {@code Runnable} or {@code Callable} subcommand.
+ * If the last (sub)command does not implement either {@code Runnable} or {@code Callable}, a {@code ExecutionException}
+ * is thrown detailing the problem and capturing the offending {@code CommandLine} object.
+ *
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return an empty list if help was requested, or a list containing a single element: the result of calling the
+ * {@code Callable}, or a {@code null} element if the last (sub)command was a {@code Runnable}
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
+ CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ return Arrays.asList(execute(last));
+ }
+ }
+ /**
+ * Command line parse result handler that prints help if requested, and otherwise executes the top-level command and
+ * all subcommands as {@code Runnable} or {@code Callable}.
+ * For use in the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) parseWithHandler} methods.
+ * @since 2.0 */
+ public static class RunAll implements IParseResultHandler {
+ /** Prints help if requested, and otherwise executes the top-level command and all subcommands as {@code Runnable}
+ * or {@code Callable}. If any of the {@code CommandLine} commands does not implement either
+ * {@code Runnable} or {@code Callable}, a {@code ExecutionException}
+ * is thrown detailing the problem and capturing the offending {@code CommandLine} object.
+ *
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return an empty list if help was requested, or a list containing the result of executing all commands:
+ * the return values from calling the {@code Callable} commands, {@code null} elements for commands that implement {@code Runnable}
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ if (printHelpIfRequested(parsedCommands, out, ansi)) {
+ return null;
+ }
+ List<Object> result = new ArrayList<Object>();
+ for (CommandLine parsed : parsedCommands) {
+ result.add(execute(parsed));
+ }
+ return result;
+ }
+ }
+ /**
+ * Returns the result of calling {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)}
+ * with {@code Help.Ansi.AUTO} and a new {@link DefaultExceptionHandler} in addition to the specified parse result handler,
+ * {@code PrintStream}, and the specified command line arguments.
+ * <p>
+ * This is a convenience method intended to offer the same ease of use as the {@link #run(Runnable, PrintStream, Help.Ansi, String...) run}
+ * and {@link #call(Callable, PrintStream, Help.Ansi, String...) call} methods, but with more flexibility and better
+ * support for nested subcommands.
+ * </p>
+ * <p>Calling this method roughly expands to:</p>
+ * <pre>
+ * try {
+ * List<CommandLine> parsedCommands = parse(args);
+ * return parseResultsHandler.handleParseResult(parsedCommands, out, Help.Ansi.AUTO);
+ * } catch (ParameterException ex) {
+ * return new DefaultExceptionHandler().handleException(ex, out, ansi, args);
+ * }
+ * </pre>
+ * <p>
+ * Picocli provides some default handlers that allow you to accomplish some common tasks with very little code.
+ * The following handlers are available:</p>
+ * <ul>
+ * <li>{@link RunLast} handler prints help if requested, and otherwise gets the last specified command or subcommand
+ * and tries to execute it as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunFirst} handler prints help if requested, and otherwise executes the top-level command as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunAll} handler prints help if requested, and otherwise executes all recognized commands and subcommands as {@code Runnable} or {@code Callable} tasks.</li>
+ * <li>{@link DefaultExceptionHandler} prints the error message followed by usage help</li>
+ * </ul>
+ * @param handler the function that will process the result of successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param args the command line arguments
+ * @return a list of results, or an empty list if there are no results
+ * @throws ExecutionException if the command line arguments were parsed successfully but a problem occurred while processing the
+ * parse results; use {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ * @see RunLast
+ * @see RunAll
+ * @since 2.0 */
+ public List<Object> parseWithHandler(IParseResultHandler handler, PrintStream out, String... args) {
+ return parseWithHandlers(handler, out, Help.Ansi.AUTO, new DefaultExceptionHandler(), args);
+ }
+ /**
+ * Tries to {@linkplain #parse(String...) parse} the specified command line arguments, and if successful, delegates
+ * the processing of the resulting list of {@code CommandLine} objects to the specified {@linkplain IParseResultHandler handler}.
+ * If the command line arguments were invalid, the {@code ParameterException} thrown from the {@code parse} method
+ * is caught and passed to the specified {@link IExceptionHandler}.
+ * <p>
+ * This is a convenience method intended to offer the same ease of use as the {@link #run(Runnable, PrintStream, Help.Ansi, String...) run}
+ * and {@link #call(Callable, PrintStream, Help.Ansi, String...) call} methods, but with more flexibility and better
+ * support for nested subcommands.
+ * </p>
+ * <p>Calling this method roughly expands to:</p>
+ * <pre>
+ * try {
+ * List<CommandLine> parsedCommands = parse(args);
+ * return parseResultsHandler.handleParseResult(parsedCommands, out, ansi);
+ * } catch (ParameterException ex) {
+ * return new exceptionHandler.handleException(ex, out, ansi, args);
+ * }
+ * </pre>
+ * <p>
+ * Picocli provides some default handlers that allow you to accomplish some common tasks with very little code.
+ * The following handlers are available:</p>
+ * <ul>
+ * <li>{@link RunLast} handler prints help if requested, and otherwise gets the last specified command or subcommand
+ * and tries to execute it as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunFirst} handler prints help if requested, and otherwise executes the top-level command as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunAll} handler prints help if requested, and otherwise executes all recognized commands and subcommands as {@code Runnable} or {@code Callable} tasks.</li>
+ * <li>{@link DefaultExceptionHandler} prints the error message followed by usage help</li>
+ * </ul>
+ *
+ * @param handler the function that will process the result of successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @param exceptionHandler the function that can handle the {@code ParameterException} thrown when the command line arguments are invalid
+ * @param args the command line arguments
+ * @return a list of results produced by the {@code IParseResultHandler} or the {@code IExceptionHandler}, or an empty list if there are no results
+ * @throws ExecutionException if the command line arguments were parsed successfully but a problem occurred while processing the parse
+ * result {@code CommandLine} objects; use {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ * @see RunLast
+ * @see RunAll
+ * @see DefaultExceptionHandler
+ * @since 2.0 */
+ public List<Object> parseWithHandlers(IParseResultHandler handler, PrintStream out, Help.Ansi ansi, IExceptionHandler exceptionHandler, String... args) {
+ try {
+ List<CommandLine> result = parse(args);
+ return handler.handleParseResult(result, out, ansi);
+ } catch (ParameterException ex) {
+ return exceptionHandler.handleException(ex, out, ansi, args);
+ }
+ }
/**
* Equivalent to {@code new CommandLine(command).usage(out)}. See {@link #usage(PrintStream)} for details.
* @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters}
@@ -426,6 +744,13 @@ public class CommandLine {
*/
public void usage(PrintStream out, Help.ColorScheme colorScheme) {
Help help = new Help(interpreter.command, colorScheme).addAllSubcommands(getSubcommands());
+ if (!Help.DEFAULT_SEPARATOR.equals(getSeparator())) {
+ help.separator = getSeparator();
+ help.parameterLabelRenderer = help.createDefaultParamLabelRenderer(); // update for new separator
+ }
+ if (!Help.DEFAULT_COMMAND_NAME.equals(getCommandName())) {
+ help.commandName = getCommandName();
+ }
StringBuilder sb = new StringBuilder()
.append(help.headerHeading())
.append(help.header())
@@ -448,6 +773,7 @@ public class CommandLine {
* Delegates to {@link #printVersionHelp(PrintStream, Help.Ansi)} with the {@linkplain Help.Ansi#AUTO platform default}.
* @param out the printStream to print to
* @see #printVersionHelp(PrintStream, Help.Ansi)
+ * @since 0.9.8
*/
public void printVersionHelp(PrintStream out) { printVersionHelp(out, Help.Ansi.AUTO); }
@@ -460,57 +786,169 @@ public class CommandLine {
* @see Command#version()
* @see Option#versionHelp()
* @see #isVersionHelpRequested()
+ * @since 0.9.8
*/
public void printVersionHelp(PrintStream out, Help.Ansi ansi) {
for (String versionInfo : versionLines) {
out.println(ansi.new Text(versionInfo));
}
}
+ /**
+ * Prints version information from the {@link Command#version()} annotation to the specified {@code PrintStream}.
+ * Each element of the array of version strings is {@linkplain String#format(String, Object...) formatted} with the
+ * specified parameters, and printed on a separate line. Both version strings and parameters may contain
+ * <a href="http://picocli.info/#_usage_help_with_styles_and_colors">markup for colors and style</a>.
+ * @param out the printStream to print to
+ * @param ansi whether the usage message should include ANSI escape codes or not
+ * @param params Arguments referenced by the format specifiers in the version strings
+ * @see Command#version()
+ * @see Option#versionHelp()
+ * @see #isVersionHelpRequested()
+ * @since 1.0.0
+ */
+ public void printVersionHelp(PrintStream out, Help.Ansi ansi, Object... params) {
+ for (String versionInfo : versionLines) {
+ out.println(ansi.new Text(String.format(versionInfo, params)));
+ }
+ }
+
+ /**
+ * Delegates to {@link #call(Callable, PrintStream, Help.Ansi, String...)} with {@link Help.Ansi#AUTO}.
+ * <p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Callable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param callable the command to call when {@linkplain #parse(String...) parsing} succeeds.
+ * @param out the printStream to print to
+ * @param args the command line arguments to parse
+ * @param <C> the annotated object must implement Callable
+ * @param <T> the return type of the most specific command (must implement {@code Callable})
+ * @see #call(Callable, PrintStream, Help.Ansi, String...)
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Callable throws an exception
+ * @return {@code null} if an error occurred while parsing the command line options, otherwise returns the result of calling the Callable
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunFirst
+ */
+ public static <C extends Callable<T>, T> T call(C callable, PrintStream out, String... args) {
+ return call(callable, out, Help.Ansi.AUTO, args);
+ }
+ /**
+ * Convenience method to allow command line application authors to avoid some boilerplate code in their application.
+ * The annotated object needs to implement {@link Callable}. Calling this method is equivalent to:
+ * <pre>
+ * CommandLine cmd = new CommandLine(callable);
+ * List<CommandLine> parsedCommands;
+ * try {
+ * parsedCommands = cmd.parse(args);
+ * } catch (ParameterException ex) {
+ * out.println(ex.getMessage());
+ * cmd.usage(out, ansi);
+ * return null;
+ * }
+ * if (CommandLine.printHelpIfRequested(parsedCommands, out, ansi)) {
+ * return null;
+ * }
+ * CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ * try {
+ * Callable<Object> subcommand = last.getCommand();
+ * return subcommand.call();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error calling " + last.getCommand(), ex);
+ * }
+ * </pre>
+ * <p>
+ * If the specified Callable command has subcommands, the {@linkplain RunLast last} subcommand specified on the
+ * command line is executed.
+ * Commands with subcommands may be interested in calling the {@link #parseWithHandler(IParseResultHandler, PrintStream, String...) parseWithHandler}
+ * method with a {@link RunAll} handler or a custom handler.
+ * </p><p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Callable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param callable the command to call when {@linkplain #parse(String...) parsing} succeeds.
+ * @param out the printStream to print to
+ * @param ansi whether the usage message should include ANSI escape codes or not
+ * @param args the command line arguments to parse
+ * @param <C> the annotated object must implement Callable
+ * @param <T> the return type of the specified {@code Callable}
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Callable throws an exception
+ * @return {@code null} if an error occurred while parsing the command line options, or if help was requested and printed. Otherwise returns the result of calling the Callable
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunLast
+ */
+ public static <C extends Callable<T>, T> T call(C callable, PrintStream out, Help.Ansi ansi, String... args) {
+ CommandLine cmd = new CommandLine(callable); // validate command outside of try-catch
+ List<Object> results = cmd.parseWithHandlers(new RunLast(), out, ansi, new DefaultExceptionHandler(), args);
+ return results == null || results.isEmpty() ? null : (T) results.get(0);
+ }
/**
* Delegates to {@link #run(Runnable, PrintStream, Help.Ansi, String...)} with {@link Help.Ansi#AUTO}.
- * @param command the command to run when {@linkplain #populateCommand(Object, String...) parsing} succeeds.
+ * <p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Runnable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param runnable the command to run when {@linkplain #parse(String...) parsing} succeeds.
* @param out the printStream to print to
* @param args the command line arguments to parse
* @param <R> the annotated object must implement Runnable
* @see #run(Runnable, PrintStream, Help.Ansi, String...)
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Runnable throws an exception
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunFirst
*/
- public static <R extends Runnable> void run(R command, PrintStream out, String... args) {
- run(command, out, Help.Ansi.AUTO, args);
+ public static <R extends Runnable> void run(R runnable, PrintStream out, String... args) {
+ run(runnable, out, Help.Ansi.AUTO, args);
}
/**
* Convenience method to allow command line application authors to avoid some boilerplate code in their application.
* The annotated object needs to implement {@link Runnable}. Calling this method is equivalent to:
* <pre>
- * CommandLine cmd = new CommandLine(command);
+ * CommandLine cmd = new CommandLine(runnable);
+ * List<CommandLine> parsedCommands;
* try {
- * cmd.parse(args);
- * } catch (Exception ex) {
- * System.err.println(ex.getMessage());
+ * parsedCommands = cmd.parse(args);
+ * } catch (ParameterException ex) {
+ * out.println(ex.getMessage());
* cmd.usage(out, ansi);
- * return;
+ * return null;
+ * }
+ * if (CommandLine.printHelpIfRequested(parsedCommands, out, ansi)) {
+ * return null;
+ * }
+ * CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ * try {
+ * Runnable subcommand = last.getCommand();
+ * subcommand.run();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error running " + last.getCommand(), ex);
* }
- * command.run();
* </pre>
- * Note that this method is not suitable for commands with subcommands.
- * @param command the command to run when {@linkplain #populateCommand(Object, String...) parsing} succeeds.
+ * <p>
+ * If the specified Runnable command has subcommands, the {@linkplain RunLast last} subcommand specified on the
+ * command line is executed.
+ * Commands with subcommands may be interested in calling the {@link #parseWithHandler(IParseResultHandler, PrintStream, String...) parseWithHandler}
+ * method with a {@link RunAll} handler or a custom handler.
+ * </p><p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Runnable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param runnable the command to run when {@linkplain #parse(String...) parsing} succeeds.
* @param out the printStream to print to
* @param ansi whether the usage message should include ANSI escape codes or not
* @param args the command line arguments to parse
* @param <R> the annotated object must implement Runnable
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Runnable throws an exception
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunLast
*/
- public static <R extends Runnable> void run(R command, PrintStream out, Help.Ansi ansi, String... args) {
- CommandLine cmd = new CommandLine(command); // validate command outside of try-catch
- try {
- cmd.parse(args);
- } catch (Exception ex) {
- out.println(ex.getMessage());
- cmd.usage(out, ansi);
- return;
- }
- command.run();
+ public static <R extends Runnable> void run(R runnable, PrintStream out, Help.Ansi ansi, String... args) {
+ CommandLine cmd = new CommandLine(runnable); // validate command outside of try-catch
+ cmd.parseWithHandlers(new RunLast(), out, ansi, new DefaultExceptionHandler(), args);
}
/**
@@ -564,16 +1002,35 @@ public class CommandLine {
return this;
}
- /** Returns the String that separates option names from option values when parsing command line options. {@code '='} by default.
+ /** Returns the String that separates option names from option values when parsing command line options. {@value Help#DEFAULT_SEPARATOR} by default.
* @return the String the parser uses to separate option names from option values */
public String getSeparator() {
return interpreter.separator;
}
/** Sets the String the parser uses to separate option names from option values to the specified value.
- * @param separator the String that separates option names from option values */
- public void setSeparator(String separator) {
+ * The separator may also be set declaratively with the {@link CommandLine.Command#separator()} annotation attribute.
+ * @param separator the String that separates option names from option values
+ * @return this {@code CommandLine} object, to allow method chaining */
+ public CommandLine setSeparator(String separator) {
interpreter.separator = Assert.notNull(separator, "separator");
+ return this;
+ }
+
+ /** Returns the command name (also called program name) displayed in the usage help synopsis. {@value Help#DEFAULT_COMMAND_NAME} by default.
+ * @return the command name (also called program name) displayed in the usage */
+ public String getCommandName() {
+ return commandName;
+ }
+
+ /** Sets the command name (also called program name) displayed in the usage help synopsis to the specified value.
+ * Note that this method only modifies the usage help message, it does not impact parsing behaviour.
+ * The command name may also be set declaratively with the {@link CommandLine.Command#name()} annotation attribute.
+ * @param commandName command name (also called program name) displayed in the usage help synopsis
+ * @return this {@code CommandLine} object, to allow method chaining */
+ public CommandLine setCommandName(String commandName) {
+ this.commandName = Assert.notNull(commandName, "commandName");
+ return this;
}
private static boolean empty(String str) { return str == null || str.trim().length() == 0; }
private static boolean empty(Object[] array) { return array == null || array.length == 0; }
@@ -581,6 +1038,35 @@ public class CommandLine {
private static String str(String[] arr, int i) { return (arr == null || arr.length == 0) ? "" : arr[i]; }
private static boolean isBoolean(Class<?> type) { return type == Boolean.class || type == Boolean.TYPE; }
private static CommandLine toCommandLine(Object obj) { return obj instanceof CommandLine ? (CommandLine) obj : new CommandLine(obj);}
+ private static boolean isMultiValue(Field field) { return isMultiValue(field.getType()); }
+ private static boolean isMultiValue(Class<?> cls) { return cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls); }
+ private static Class<?>[] getTypeAttribute(Field field) {
+ Class<?>[] explicit = field.isAnnotationPresent(Parameters.class) ? field.getAnnotation(Parameters.class).type() : field.getAnnotation(Option.class).type();
+ if (explicit.length > 0) { return explicit; }
+ if (field.getType().isArray()) { return new Class<?>[] { field.getType().getComponentType() }; }
+ if (isMultiValue(field)) {
+ Type type = field.getGenericType(); // e.g. Map<Long, ? extends Number>
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Type[] paramTypes = parameterizedType.getActualTypeArguments(); // e.g. ? extends Number
+ Class<?>[] result = new Class<?>[paramTypes.length];
+ for (int i = 0; i < paramTypes.length; i++) {
+ if (paramTypes[i] instanceof Class) { result[i] = (Class<?>) paramTypes[i]; continue; } // e.g. Long
+ if (paramTypes[i] instanceof WildcardType) { // e.g. ? extends Number
+ WildcardType wildcardType = (WildcardType) paramTypes[i];
+ Type[] lower = wildcardType.getLowerBounds(); // e.g. []
+ if (lower.length > 0 && lower[0] instanceof Class) { result[i] = (Class<?>) lower[0]; continue; }
+ Type[] upper = wildcardType.getUpperBounds(); // e.g. Number
+ if (upper.length > 0 && upper[0] instanceof Class) { result[i] = (Class<?>) upper[0]; continue; }
+ }
+ Arrays.fill(result, String.class); return result; // too convoluted generic type, giving up
+ }
+ return result; // we inferred all types from ParameterizedType
+ }
+ return new Class<?>[] {String.class, String.class}; // field is multi-value but not ParameterizedType
+ }
+ return new Class<?>[] {field.getType()}; // not a multi-value field
+ }
/**
* <p>
* Annotate fields in your class with {@code @Option} and picocli will initialize these fields when matching
@@ -600,10 +1086,10 @@ public class CommandLine {
* @Option(names = { "-v", "--verbose"}, description = "Verbosely list files processed")
* private boolean verbose;
*
- * @Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit")
+ * @Option(names = { "-h", "--help", "-?", "-help"}, usageHelp = true, description = "Display this help and exit")
* private boolean help;
*
- * @Option(names = { "-V", "--version"}, help = true, description = "Display version information and exit")
+ * @Option(names = { "-V", "--version"}, versionHelp = true, description = "Display version information and exit")
* private boolean version;
* }
* </pre>
@@ -681,6 +1167,7 @@ public class CommandLine {
* and take the appropriate action.
* </p>
* @return whether this option disables validation of the other arguments
+ * @deprecated Use {@link #usageHelp()} and {@link #versionHelp()} instead. See {@link #printHelpIfRequested(List, PrintStream, CommandLine.Help.Ansi)}
*/
boolean help() default false;
@@ -698,6 +1185,7 @@ public class CommandLine {
* and take the appropriate action.
* </p>
* @return whether this option allows the user to request usage help
+ * @since 0.9.8
*/
boolean usageHelp() default false;
@@ -715,6 +1203,7 @@ public class CommandLine {
* and take the appropriate action.
* </p>
* @return whether this option allows the user to request version information
+ * @since 0.9.8
*/
boolean versionHelp() default false;
@@ -783,22 +1272,34 @@ public class CommandLine {
*/
String paramLabel() default "";
- /**
- * <p>
- * Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings.
+ /** <p>
+ * Optionally specify a {@code type} to control exactly what Class the option parameter should be converted
+ * to. This may be useful when the field type is an interface or an abstract class. For example, a field can
+ * be declared to have type {@code java.lang.Number}, and annotating {@code @Option(type=Short.class)}
+ * ensures that the option parameter value is converted to a {@code Short} before setting the field value.
* </p><p>
- * If the field's type is a {@code Collection}, the generic type parameter of the collection is erased and
- * cannot be determined at runtime. Specify a {@code type} attribute to store values other than String in
- * the Collection. Picocli will use the {@link ITypeConverter}
- * that is {@linkplain #registerConverter(Class, ITypeConverter) registered} for that type to convert
- * the raw String values before they are added to the collection.
+ * For array fields whose <em>component</em> type is an interface or abstract class, specify the concrete <em>component</em> type.
+ * For example, a field with type {@code Number[]} may be annotated with {@code @Option(type=Short.class)}
+ * to ensure that option parameter values are converted to {@code Short} before adding an element to the array.
* </p><p>
- * When the field's type is an array, the {@code type} attribute is ignored: the values will be converted
- * to the array component type and the array will be replaced with a new instance containing both the old and
- * the new values. </p>
- * @return the type to convert the raw String values to before adding them to the Collection
+ * Picocli will use the {@link ITypeConverter} that is
+ * {@linkplain #registerConverter(Class, ITypeConverter) registered} for the specified type to convert
+ * the raw String values before modifying the field value.
+ * </p><p>
+ * Prior to 2.0, the {@code type} attribute was necessary for {@code Collection} and {@code Map} fields,
+ * but starting from 2.0 picocli will infer the component type from the generic type's type arguments.
+ * For example, for a field of type {@code Map<TimeUnit, Long>} picocli will know the option parameter
+ * should be split up in key=value pairs, where the key should be converted to a {@code java.util.concurrent.TimeUnit}
+ * enum value, and the value should be converted to a {@code Long}. No {@code @Option(type=...)} type attribute
+ * is required for this. For generic types with wildcards, picocli will take the specified upper or lower bound
+ * as the Class to convert to, unless the {@code @Option} annotation specifies an explicit {@code type} attribute.
+ * </p><p>
+ * If the field type is a raw collection or a raw map, and you want it to contain other values than Strings,
+ * or if the generic type's type arguments are interfaces or abstract classes, you may
+ * specify a {@code type} attribute to control the Class that the option parameter should be converted to.
+ * @return the type(s) to convert the raw String values
*/
- Class<?> type() default String.class;
+ Class<?>[] type() default {};
/**
* Specify a regular expression to use to split option parameter values before applying them to the field.
@@ -881,20 +1382,33 @@ public class CommandLine {
/**
* <p>
- * Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings.
+ * Optionally specify a {@code type} to control exactly what Class the positional parameter should be converted
+ * to. This may be useful when the field type is an interface or an abstract class. For example, a field can
+ * be declared to have type {@code java.lang.Number}, and annotating {@code @Parameters(type=Short.class)}
+ * ensures that the positional parameter value is converted to a {@code Short} before setting the field value.
+ * </p><p>
+ * For array fields whose <em>component</em> type is an interface or abstract class, specify the concrete <em>component</em> type.
+ * For example, a field with type {@code Number[]} may be annotated with {@code @Parameters(type=Short.class)}
+ * to ensure that positional parameter values are converted to {@code Short} before adding an element to the array.
* </p><p>
- * If the field's type is a {@code Collection}, the generic type parameter of the collection is erased and
- * cannot be determined at runtime. Specify a {@code type} attribute to store values other than String in
- * the Collection. Picocli will use the {@link ITypeConverter}
- * that is {@linkplain #registerConverter(Class, ITypeConverter) registered} for that type to convert
- * the raw String values before they are added to the collection.
+ * Picocli will use the {@link ITypeConverter} that is
+ * {@linkplain #registerConverter(Class, ITypeConverter) registered} for the specified type to convert
+ * the raw String values before modifying the field value.
* </p><p>
- * When the field's type is an array, the {@code type} attribute is ignored: the values will be converted
- * to the array component type and the array will be replaced with a new instance containing both the old and
- * the new values. </p>
- * @return the type to convert the raw String values to before adding them to the Collection
+ * Prior to 2.0, the {@code type} attribute was necessary for {@code Collection} and {@code Map} fields,
+ * but starting from 2.0 picocli will infer the component type from the generic type's type arguments.
+ * For example, for a field of type {@code Map<TimeUnit, Long>} picocli will know the positional parameter
+ * should be split up in key=value pairs, where the key should be converted to a {@code java.util.concurrent.TimeUnit}
+ * enum value, and the value should be converted to a {@code Long}. No {@code @Parameters(type=...)} type attribute
+ * is required for this. For generic types with wildcards, picocli will take the specified upper or lower bound
+ * as the Class to convert to, unless the {@code @Parameters} annotation specifies an explicit {@code type} attribute.
+ * </p><p>
+ * If the field type is a raw collection or a raw map, and you want it to contain other values than Strings,
+ * or if the generic type's type arguments are interfaces or abstract classes, you may
+ * specify a {@code type} attribute to control the Class that the positional parameter should be converted to.
+ * @return the type(s) to convert the raw String values
*/
- Class<?> type() default String.class;
+ Class<?>[] type() default {};
/**
* Specify a regular expression to use to split positional parameter values before applying them to the field.
@@ -936,7 +1450,7 @@ public class CommandLine {
* <li>[footer]</li>
* </ul> */
@Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
+ @Target({ElementType.TYPE, ElementType.LOCAL_VARIABLE, ElementType.PACKAGE})
public @interface Command {
/** Program name to show in the synopsis. If omitted, {@code "<main class>"} is used.
* For {@linkplain #subcommands() declaratively added} subcommands, this attribute is also used
@@ -1174,19 +1688,41 @@ public class CommandLine {
: new Range(0, 0, false, true, "0");
}
static Range adjustForType(Range result, Field field) {
- return result.isUnspecified ? defaultArity(field.getType()) : result;
+ return result.isUnspecified ? defaultArity(field) : result;
}
- /** Returns a new {@code Range} based on the specified type: booleans have arity 0, arrays or Collections have
+ /** Returns the default arity {@code Range}: for {@link Option options} this is 0 for booleans and 1 for
+ * other types, for {@link Parameters parameters} booleans have arity 0, arrays or Collections have
* arity "0..*", and other types have arity 1.
+ * @param field the field whose default arity to return
+ * @return a new {@code Range} indicating the default arity of the specified field
+ * @since 2.0 */
+ public static Range defaultArity(Field field) {
+ Class<?> type = field.getType();
+ if (field.isAnnotationPresent(Option.class)) {
+ return defaultArity(type);
+ }
+ if (isMultiValue(type)) {
+ return Range.valueOf("0..1");
+ }
+ return Range.valueOf("1");// for single-valued fields (incl. boolean positional parameters)
+ }
+ /** Returns the default arity {@code Range} for {@link Option options}: booleans have arity 0, other types have arity 1.
* @param type the type whose default arity to return
* @return a new {@code Range} indicating the default arity of the specified type */
public static Range defaultArity(Class<?> type) {
- if (isBoolean(type)) {
- return Range.valueOf("0");
- } else if (type.isArray() || Collection.class.isAssignableFrom(type)) {
- return Range.valueOf("0..*");
- }
- return Range.valueOf("1");// for single-valued fields
+ return isBoolean(type) ? Range.valueOf("0") : Range.valueOf("1");
+ }
+ private int size() { return 1 + max - min; }
+ static Range parameterCapacity(Field field) {
+ Range arity = parameterArity(field);
+ if (!isMultiValue(field)) { return arity; }
+ Range index = parameterIndex(field);
+ if (arity.max == 0) { return arity; }
+ if (index.size() == 1) { return arity; }
+ if (index.isVariable) { return Range.valueOf(arity.min + "..*"); }
+ if (arity.size() == 1) { return Range.valueOf(arity.min * index.size() + ""); }
+ if (arity.isVariable) { return Range.valueOf(arity.min * index.size() + "..*"); }
+ return Range.valueOf(arity.min * index.size() + ".." + arity.max * index.size());
}
/** Leniently parses the specified String as an {@code Range} value and return the result. A range string can
* be a fixed integer value or a range of the form {@code MIN_VALUE + ".." + MAX_VALUE}. If the
@@ -1231,6 +1767,13 @@ public class CommandLine {
* @return a new Range object with the specified {@code max} value */
public Range max(int newMax) { return new Range(Math.min(min, newMax), newMax, isVariable, isUnspecified, originalValue); }
+ /**
+ * Returns {@code true} if this Range includes the specified value, {@code false} otherwise.
+ * @param value the value to check
+ * @return {@code true} if the specified value is not less than the minimum and not greater than the maximum of this Range
+ */
+ public boolean contains(int value) { return min <= value && max >= value; }
+
public boolean equals(Object object) {
if (!(object instanceof Range)) { return false; }
Range other = (Range) object;
@@ -1247,11 +1790,11 @@ public class CommandLine {
return (result == 0) ? max - other.max : result;
}
}
- private static void init(Class<?> cls,
- List<Field> requiredFields,
- Map<String, Field> optionName2Field,
- Map<Character, Field> singleCharOption2Field,
- List<Field> positionalParametersFields) {
+ static void init(Class<?> cls,
+ List<Field> requiredFields,
+ Map<String, Field> optionName2Field,
+ Map<Character, Field> singleCharOption2Field,
+ List<Field> positionalParametersFields) {
Field[] declaredFields = cls.getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
@@ -1276,7 +1819,7 @@ public class CommandLine {
}
if (field.isAnnotationPresent(Parameters.class)) {
if (field.isAnnotationPresent(Option.class)) {
- throw new ParameterException("A field can be either @Option or @Parameters, but '"
+ throw new DuplicateOptionAnnotationsException("A field can be either @Option or @Parameters, but '"
+ field.getName() + "' is both.");
}
positionalParametersFields.add(field);
@@ -1315,7 +1858,8 @@ public class CommandLine {
private final List<Field> positionalParametersFields = new ArrayList<Field>();
private final Object command;
private boolean isHelpRequested;
- private String separator = "=";
+ private String separator = Help.DEFAULT_SEPARATOR;
+ private int position;
Interpreter(Object command) {
converterRegistry.put(Path.class, new BuiltIn.PathConverter());
@@ -1350,9 +1894,10 @@ public class CommandLine {
converterRegistry.put(Pattern.class, new BuiltIn.PatternConverter());
converterRegistry.put(UUID.class, new BuiltIn.UUIDConverter());
- this.command = Assert.notNull(command, "command");
- Class<?> cls = command.getClass();
- String declaredSeparator = null;
+ this.command = Assert.notNull(command, "command");
+ Class<?> cls = command.getClass();
+ String declaredName = null;
+ String declaredSeparator = null;
boolean hasCommandAnnotation = false;
while (cls != null) {
init(cls, requiredFields, optionName2Field, singleCharOption2Field, positionalParametersFields);
@@ -1360,12 +1905,13 @@ public class CommandLine {
hasCommandAnnotation = true;
Command cmd = cls.getAnnotation(Command.class);
declaredSeparator = (declaredSeparator == null) ? cmd.separator() : declaredSeparator;
+ declaredName = (declaredName == null) ? cmd.name() : declaredName;
CommandLine.this.versionLines.addAll(Arrays.asList(cmd.version()));
for (Class<?> sub : cmd.subcommands()) {
Command subCommand = sub.getAnnotation(Command.class);
if (subCommand == null || Help.DEFAULT_COMMAND_NAME.equals(subCommand.name())) {
- throw new IllegalArgumentException("Subcommand " + sub.getName() +
+ throw new InitializationException("Subcommand " + sub.getName() +
" is missing the mandatory @Command annotation with a 'name' attribute");
}
try {
@@ -1375,11 +1921,11 @@ public class CommandLine {
commandLine.parent = CommandLine.this;
commands.put(subCommand.name(), commandLine);
}
- catch (IllegalArgumentException ex) { throw ex; }
- catch (NoSuchMethodException ex) { throw new IllegalArgumentException("Cannot instantiate subcommand " +
+ catch (InitializationException ex) { throw ex; }
+ catch (NoSuchMethodException ex) { throw new InitializationException("Cannot instantiate subcommand " +
sub.getName() + ": the class has no constructor", ex); }
catch (Exception ex) {
- throw new IllegalStateException("Could not instantiate and add subcommand " +
+ throw new InitializationException("Could not instantiate and add subcommand " +
sub.getName() + ": " + ex, ex);
}
}
@@ -1387,11 +1933,12 @@ public class CommandLine {
cls = cls.getSuperclass();
}
separator = declaredSeparator != null ? declaredSeparator : separator;
+ CommandLine.this.commandName = declaredName != null ? declaredName : CommandLine.this.commandName;
Collections.sort(positionalParametersFields, new PositionalParametersSorter());
validatePositionalParameters(positionalParametersFields);
if (positionalParametersFields.isEmpty() && optionName2Field.isEmpty() && !hasCommandAnnotation) {
- throw new IllegalArgumentException(command + " (" + command.getClass() +
+ throw new InitializationException(command + " (" + command.getClass() +
") is not a command: it has no @Command, @Option or @Parameters annotations");
}
}
@@ -1404,6 +1951,7 @@ public class CommandLine {
*/
List<CommandLine> parse(String... args) {
Assert.notNull(args, "argument array");
+ if (tracer.isInfo()) {tracer.info("Parsing %d command line args %s%n", args.length, Arrays.toString(args));}
Stack<String> arguments = new Stack<String>();
for (int i = args.length - 1; i >= 0; i--) {
arguments.push(args[i]);
@@ -1419,6 +1967,8 @@ public class CommandLine {
CommandLine.this.versionHelpRequested = false;
CommandLine.this.usageHelpRequested = false;
+ Class<?> cmdClass = this.command.getClass();
+ if (tracer.isDebug()) {tracer.debug("Initializing %s: %d options, %d positional parameters, %d required, %d subcommands.%n", cmdClass.getName(), new HashSet<Field>(optionName2Field.values()).size(), positionalParametersFields.size(), requiredFields.size(), commands.size());}
parsedCommands.add(CommandLine.this);
List<Field> required = new ArrayList<Field>(requiredFields);
Set<Field> initialized = new HashSet<Field>();
@@ -1428,20 +1978,23 @@ public class CommandLine {
} catch (ParameterException ex) {
throw ex;
} catch (Exception ex) {
- int offendingArgIndex = originalArgs.length - argumentStack.size();
+ int offendingArgIndex = originalArgs.length - argumentStack.size() - 1;
String arg = offendingArgIndex >= 0 && offendingArgIndex < originalArgs.length ? originalArgs[offendingArgIndex] : "?";
- throw ParameterException.create(ex, arg, argumentStack.size(), originalArgs);
+ throw ParameterException.create(CommandLine.this, ex, arg, offendingArgIndex, originalArgs);
}
if (!isAnyHelpRequested() && !required.isEmpty()) {
- if (required.get(0).isAnnotationPresent(Option.class)) {
- throw MissingParameterException.create(required);
- } else {
- try {
- processPositionalParameters0(required, true, new Stack<String>());
- } catch (ParameterException ex) { throw ex;
- } catch (Exception ex) { throw new IllegalStateException("Internal error: " + ex, ex); }
+ for (Field missing : required) {
+ if (missing.isAnnotationPresent(Option.class)) {
+ throw MissingParameterException.create(CommandLine.this, required, separator);
+ } else {
+ assertNoMissingParameters(missing, Range.parameterArity(missing).min, argumentStack);
+ }
}
}
+ if (!unmatchedArguments.isEmpty()) {
+ if (!isUnmatchedArgumentsAllowed()) { throw new UnmatchedArgumentException(CommandLine.this, unmatchedArguments); }
+ if (tracer.isWarn()) { tracer.warn("Unmatched arguments: %s%n", unmatchedArguments); }
+ }
}
private void processArguments(List<CommandLine> parsedCommands,
@@ -1460,19 +2013,22 @@ public class CommandLine {
while (!args.isEmpty()) {
String arg = args.pop();
+ if (tracer.isDebug()) {tracer.debug("Processing argument '%s'. Remainder=%s%n", arg, reverse((Stack<String>) args.clone()));}
// Double-dash separates options from positional arguments.
// If found, then interpret the remaining args as positional parameters.
if ("--".equals(arg)) {
- processPositionalParameters(required, args);
+ tracer.info("Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n");
+ processRemainderAsPositionalParameters(required, initialized, args);
return; // we are done
}
// if we find another command, we are done with the current command
if (commands.containsKey(arg)) {
if (!isHelpRequested && !required.isEmpty()) { // ensure current command portion is valid
- throw MissingParameterException.create(required);
+ throw MissingParameterException.create(CommandLine.this, required, separator);
}
+ if (tracer.isDebug()) {tracer.debug("Found subcommand '%s' (%s)%n", arg, commands.get(arg).interpreter.command.getClass().getName());}
commands.get(arg).interpreter.parse(parsedCommands, args, originalArgs);
return; // remainder done by the command
}
@@ -1491,7 +2047,12 @@ public class CommandLine {
String optionParam = arg.substring(separatorIndex + separator.length());
args.push(optionParam);
arg = key;
+ if (tracer.isDebug()) {tracer.debug("Separated '%s' option from '%s' option parameter%n", key, optionParam);}
+ } else {
+ if (tracer.isDebug()) {tracer.debug("'%s' contains separator '%s' but '%s' is not a known option%n", arg, separator, key);}
}
+ } else {
+ if (tracer.isDebug()) {tracer.debug("'%s' cannot be separated into <option>%s<option-parameter>%n", arg, separator);}
}
if (optionName2Field.containsKey(arg)) {
processStandaloneOption(required, initialized, arg, args, paramAttachedToOption);
@@ -1499,56 +2060,66 @@ public class CommandLine {
// Compact (single-letter) options can be grouped with other options or with an argument.
// only single-letter options can be combined with other options or with an argument
else if (arg.length() > 2 && arg.startsWith("-")) {
+ if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as clustered short options%n", arg, args);}
processClusteredShortOptions(required, initialized, arg, args);
}
// The argument could not be interpreted as an option.
// We take this to mean that the remainder are positional arguments
else {
args.push(arg);
- processPositionalParameters(required, args);
- return;
+ if (tracer.isDebug()) {tracer.debug("Could not find option '%s', deciding whether to treat as unmatched option or positional parameter...%n", arg);}
+ if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); continue; } // #149
+ if (tracer.isDebug()) {tracer.debug("No option named '%s' found. Processing remainder as positional parameters%n", arg);}
+ processPositionalParameter(required, initialized, args);
}
}
}
-
- private void processPositionalParameters(Collection<Field> required, Stack<String> args) throws Exception {
- processPositionalParameters0(required, false, args);
- if (!args.empty()) {
- handleUnmatchedArguments(args);
- return;
- };
+ private boolean resemblesOption(String arg) {
+ int count = 0;
+ for (String optionName : optionName2Field.keySet()) {
+ for (int i = 0; i < arg.length(); i++) {
+ if (optionName.length() > i && arg.charAt(i) == optionName.charAt(i)) { count++; } else { break; }
+ }
+ }
+ boolean result = count > 0 && count * 10 >= optionName2Field.size() * 9; // at least one prefix char in common with 9 out of 10 options
+ if (tracer.isDebug()) {tracer.debug("%s %s an option: %d matching prefix chars out of %d option names%n", arg, (result ? "resembles" : "doesn't resemble"), count, optionName2Field.size());}
+ return result;
}
-
+ private void handleUnmatchedArguments(String arg) {Stack<String> args = new Stack<String>(); args.add(arg); handleUnmatchedArguments(args);}
private void handleUnmatchedArguments(Stack<String> args) {
- if (!isUnmatchedArgumentsAllowed()) { throw new UnmatchedArgumentException(args); }
while (!args.isEmpty()) { unmatchedArguments.add(args.pop()); } // addAll would give args in reverse order
}
- private void processPositionalParameters0(Collection<Field> required, boolean validateOnly, Stack<String> args) throws Exception {
- int max = -1;
+ private void processRemainderAsPositionalParameters(Collection<Field> required, Set<Field> initialized, Stack<String> args) throws Exception {
+ while (!args.empty()) {
+ processPositionalParameter(required, initialized, args);
+ }
+ }
+ private void processPositionalParameter(Collection<Field> required, Set<Field> initialized, Stack<String> args) throws Exception {
+ if (tracer.isDebug()) {tracer.debug("Processing next arg as a positional parameter at index=%d. Remainder=%s%n", position, reverse((Stack<String>) args.clone()));}
+ int consumed = 0;
for (Field positionalParam : positionalParametersFields) {
Range indexRange = Range.parameterIndex(positionalParam);
- max = Math.max(max, indexRange.max);
- @SuppressWarnings("unchecked")
- Stack<String> argsCopy = reverse((Stack<String>) args.clone());
- if (!indexRange.isVariable) {
- for (int i = argsCopy.size() - 1; i > indexRange.max; i--) {
- argsCopy.removeElementAt(i);
- }
+ if (!indexRange.contains(position)) {
+ continue;
}
- Collections.reverse(argsCopy);
- for (int i = 0; i < indexRange.min && !argsCopy.isEmpty(); i++) { argsCopy.pop(); }
+ @SuppressWarnings("unchecked")
+ Stack<String> argsCopy = (Stack<String>) args.clone();
Range arity = Range.parameterArity(positionalParam);
+ if (tracer.isDebug()) {tracer.debug("Position %d is in index range %s. Trying to assign args to %s, arity=%s%n", position, indexRange, positionalParam, arity);}
assertNoMissingParameters(positionalParam, arity.min, argsCopy);
- if (!validateOnly) {
- applyOption(positionalParam, Parameters.class, arity, false, argsCopy, null);
- required.remove(positionalParam);
- }
+ int originalSize = argsCopy.size();
+ applyOption(positionalParam, Parameters.class, arity, false, argsCopy, initialized, "args[" + indexRange + "]");
+ int count = originalSize - argsCopy.size();
+ if (count > 0) { required.remove(positionalParam); }
+ consumed = Math.max(consumed, count);
}
// remove processed args from the stack
- if (!validateOnly && !positionalParametersFields.isEmpty()) {
- int processedArgCount = Math.min(args.size(), max < Integer.MAX_VALUE ? max + 1 : Integer.MAX_VALUE);
- for (int i = 0; i < processedArgCount; i++) { args.pop(); }
+ for (int i = 0; i < consumed; i++) { args.pop(); }
+ position += consumed;
+ if (tracer.isDebug()) {tracer.debug("Consumed %d arguments, moving position to index %d.%n", consumed, position);}
+ if (consumed == 0 && !args.isEmpty()) {
+ handleUnmatchedArguments(args.pop());
}
}
@@ -1563,7 +2134,8 @@ public class CommandLine {
if (paramAttachedToKey) {
arity = arity.min(Math.max(1, arity.min)); // if key=value, minimum arity is at least 1
}
- applyOption(field, Option.class, arity, paramAttachedToKey, args, initialized);
+ if (tracer.isDebug()) {tracer.debug("Found option named '%s': field %s, arity=%s%n", arg, field, arity);}
+ applyOption(field, Option.class, arity, paramAttachedToKey, args, initialized, "option " + arg);
}
private void processClusteredShortOptions(Collection<Field> required,
@@ -1577,19 +2149,27 @@ public class CommandLine {
do {
if (cluster.length() > 0 && singleCharOption2Field.containsKey(cluster.charAt(0))) {
Field field = singleCharOption2Field.get(cluster.charAt(0));
+ Range arity = Range.optionArity(field);
+ String argDescription = "option " + prefix + cluster.charAt(0);
+ if (tracer.isDebug()) {tracer.debug("Found option '%s%s' in %s: field %s, arity=%s%n", prefix, cluster.charAt(0), arg, field, arity);}
required.remove(field);
cluster = cluster.length() > 0 ? cluster.substring(1) : "";
paramAttachedToOption = cluster.length() > 0;
- Range arity = Range.optionArity(field);
if (cluster.startsWith(separator)) {// attached with separator, like -f=FILE or -v=true
cluster = cluster.substring(separator.length());
arity = arity.min(Math.max(1, arity.min)); // if key=value, minimum arity is at least 1
}
+ if (arity.min > 0 && !empty(cluster)) {
+ if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as option parameter%n", cluster);}
+ }
args.push(cluster); // interpret remainder as option parameter (CAUTION: may be empty string!)
// arity may be >= 1, or
// arity <= 0 && !cluster.startsWith(separator)
// e.g., boolean @Option("-v", arity=0, varargs=true); arg "-rvTRUE", remainder cluster="TRUE"
- int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized);
+ if (!args.isEmpty() && args.peek().length() == 0 && !paramAttachedToOption) {
+ args.pop(); // throw out empty string we get at the end of a group of clustered short options
+ }
+ int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized, argDescription);
// only return if cluster (and maybe more) was consumed, otherwise continue do-while loop
if (consumed > 0) {
return;
@@ -1602,12 +2182,21 @@ public class CommandLine {
// We get here when the remainder of the cluster group is neither an option,
// nor a parameter that the last option could consume.
if (arg.endsWith(cluster)) {
- // remainder was part of a clustered group that could not be completely parsed
args.push(paramAttachedToOption ? prefix + cluster : cluster);
- handleUnmatchedArguments(args);
+ if (args.peek().equals(arg)) { // #149 be consistent between unmatched short and long options
+ if (tracer.isDebug()) {tracer.debug("Could not match any short options in %s, deciding whether to treat as unmatched option or positional parameter...%n", arg);}
+ if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); return; } // #149
+ processPositionalParameter(required, initialized, args);
+ return;
+ }
+ // remainder was part of a clustered group that could not be completely parsed
+ if (tracer.isDebug()) {tracer.debug("No option found for %s in %s%n", cluster, arg);}
+ handleUnmatchedArguments(args.pop());
+ } else {
+ args.push(cluster);
+ if (tracer.isDebug()) {tracer.debug("%s is not an option parameter for %s%n", cluster, arg);}
+ processPositionalParameter(required, initialized, args);
}
- args.push(cluster);
- processPositionalParameters(required, args);
return;
}
} while (true);
@@ -1618,28 +2207,32 @@ public class CommandLine {
Range arity,
boolean valueAttachedToOption,
Stack<String> args,
- Set<Field> initialized) throws Exception {
+ Set<Field> initialized,
+ String argDescription) throws Exception {
updateHelpRequested(field);
- if (!args.isEmpty() && args.peek().length() == 0 && !valueAttachedToOption) {
- args.pop(); // throw out empty string we get at the end of a group of clustered short options
- }
int length = args.size();
assertNoMissingParameters(field, arity.min, args);
Class<?> cls = field.getType();
if (cls.isArray()) {
- return applyValuesToArrayField(field, annotation, arity, args, cls);
+ return applyValuesToArrayField(field, annotation, arity, args, cls, argDescription);
}
if (Collection.class.isAssignableFrom(cls)) {
- return applyValuesToCollectionField(field, annotation, arity, args, cls);
+ return applyValuesToCollectionField(field, annotation, arity, args, cls, argDescription);
+ }
+ if (Map.class.isAssignableFrom(cls)) {
+ return applyValuesToMapField(field, annotation, arity, args, cls, argDescription);
}
- return applyValueToSingleValuedField(field, arity, args, cls, initialized);
+ cls = getTypeAttribute(field)[0]; // field may be interface/abstract type, use annotation to get concrete type
+ return applyValueToSingleValuedField(field, arity, args, cls, initialized, argDescription);
}
+
private int applyValueToSingleValuedField(Field field,
Range arity,
Stack<String> args,
Class<?> cls,
- Set<Field> initialized) throws Exception {
+ Set<Field> initialized,
+ String argDescription) throws Exception {
boolean noMoreValues = args.isEmpty();
String value = args.isEmpty() ? null : trim(args.pop()); // unquote the value
int result = arity.min; // the number or args we need to consume
@@ -1661,28 +2254,117 @@ public class CommandLine {
if (noMoreValues && value == null) {
return 0;
}
+ ITypeConverter<?> converter = getTypeConverter(cls, field);
+ Object newValue = tryConvert(field, -1, converter, value, cls);
+ Object oldValue = field.get(command);
+ TraceLevel level = TraceLevel.INFO;
+ String traceMessage = "Setting %s field '%s.%s' to '%5$s' (was '%4$s') for %6$s%n";
if (initialized != null) {
- if (initialized.contains(field) && !isOverwrittenOptionsAllowed()) {
- throw new OverwrittenOptionException(optionDescription("", field, 0) + " should be specified only once");
+ if (initialized.contains(field)) {
+ if (!isOverwrittenOptionsAllowed()) {
+ throw new OverwrittenOptionException(CommandLine.this, optionDescription("", field, 0) + " should be
<TRUNCATED>
[2/5] logging-log4j2 git commit: LOG4J2-2088 Upgrade picocli to 2.0
from 0.9.8
Posted by rp...@apache.org.
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineTest.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineTest.java
index 43e0732..5b482a4 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/tools/picocli/CommandLineTest.java
@@ -16,9 +16,6 @@
*/
package org.apache.logging.log4j.core.tools.picocli;
-import org.junit.Ignore;
-import org.junit.Test;
-
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
@@ -40,18 +37,28 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.SortedSet;
+import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
import static java.util.concurrent.TimeUnit.*;
import static org.junit.Assert.*;
import static org.apache.logging.log4j.core.tools.picocli.CommandLine.*;
@@ -89,9 +96,15 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.*;
// DONE -vrx, -vro outputFile, -vrooutputFile, -vro=outputFile, -vro:outputFile, -vro=, -vro:, -vro
// DONE --out outputFile, --out=outputFile, --out:outputFile, --out=, --out:, --out
public class CommandLineTest {
+ @Before public void setUp() { System.clearProperty("picocli.trace"); }
+ @After public void tearDown() { System.clearProperty("picocli.trace"); }
+
+ private static void setTraceLevel(String level) {
+ System.setProperty("picocli.trace", level);
+ }
@Test
public void testVersion() {
- assertEquals("0.9.8", CommandLine.VERSION);
+ assertEquals("2.0.0", CommandLine.VERSION);
}
private static class SupportedTypes {
@@ -562,7 +575,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new EnumParams(), "-timeUnitArray", "a", "b");
fail("Accepted invalid timeunit");
} catch (Exception ex) {
- assertEquals("Could not convert 'a' to TimeUnit[] for option '-timeUnitArray' at index 0 (timeUnitArray)" +
+ assertEquals("Could not convert 'a' to TimeUnit for option '-timeUnitArray' at index 0 (<timeUnitArray>)" +
": java.lang.IllegalArgumentException: No enum constant java.util.concurrent.TimeUnit.a", ex.getMessage());
}
}
@@ -572,7 +585,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new EnumParams(), "-timeUnitList", "DAYS", "b", "c");
fail("Accepted invalid timeunit");
} catch (Exception ex) {
- assertEquals("Could not convert 'b' to TimeUnit for option '-timeUnitList' at index 1 (timeUnitList)" +
+ assertEquals("Could not convert 'b' to TimeUnit for option '-timeUnitList' at index 1 (<timeUnitList>)" +
": java.lang.IllegalArgumentException: No enum constant java.util.concurrent.TimeUnit.b",
ex.getMessage());
}
@@ -774,7 +787,7 @@ public class CommandLineTest {
new CommandLine(new DuplicateOptions());
}
- @Test(expected = ParameterException.class)
+ @Test(expected = DuplicateOptionAnnotationsException.class)
public void testClashingAnnotationsAreRejected() {
class ClashingAnnotation {
@Option(names = "-o")
@@ -801,6 +814,7 @@ public class CommandLineTest {
}
@Test
public void testLastValueSelectedIfOptionSpecifiedMultipleTimes() {
+ setTraceLevel("OFF");
CommandLine cmd = new CommandLine(new PrivateFinalOptionFields()).setOverwrittenOptionsAllowed(true);
cmd.parse("-f", "111", "-f", "222");
PrivateFinalOptionFields ff = (PrivateFinalOptionFields) cmd.getCommand();
@@ -837,7 +851,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new RequiredField(), "arg1", "arg2");
fail("Missing required field should have thrown exception");
} catch (MissingParameterException ex) {
- assertEquals("Missing required option 'required'", ex.getMessage());
+ assertEquals("Missing required option '--required=<required>'", ex.getMessage());
}
}
@Test
@@ -892,7 +906,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new Example(), new String[0]);
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: mandatory", ex.getMessage());
+ assertEquals("Missing required parameter: <mandatory>", ex.getMessage());
}
}
@Test
@@ -906,13 +920,13 @@ public class CommandLineTest {
CommandLine.populateCommand(new Tricky1(), new String[0]);
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameters: mandatory, anotherMandatory", ex.getMessage());
+ assertEquals("Missing required parameters: <mandatory>, <anotherMandatory>", ex.getMessage());
}
try {
CommandLine.populateCommand(new Tricky1(), new String[] {"firstonly"});
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: anotherMandatory", ex.getMessage());
+ assertEquals("Missing required parameter: <anotherMandatory>", ex.getMessage());
}
}
@Test
@@ -929,7 +943,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new Tricky2(), new String[0]);
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: mandatory", ex.getMessage());
+ assertEquals("Missing required parameter: <mandatory>", ex.getMessage());
}
}
@Test
@@ -944,14 +958,14 @@ public class CommandLineTest {
CommandLine.populateCommand(new Tricky3(), new String[] {"-t", "-v", "mandatory"});
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: alsoMandatory", ex.getMessage());
+ assertEquals("Missing required parameter: <alsoMandatory>", ex.getMessage());
}
try {
CommandLine.populateCommand(new Tricky3(), new String[] { "-t", "-v"});
fail("Should not accept missing two mandatory parameters");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameters: mandatory, alsoMandatory", ex.getMessage());
+ assertEquals("Missing required parameters: <mandatory>, <alsoMandatory>", ex.getMessage());
}
}
@Test
@@ -964,7 +978,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new Tricky3(), new String[] {"-t"});
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: mandatory", ex.getMessage());
+ assertEquals("Missing required parameter: <mandatory>", ex.getMessage());
}
}
@Test
@@ -984,7 +998,27 @@ public class CommandLineTest {
CommandLine.populateCommand(new App(), new String[0]);
fail("Should not accept missing mandatory parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameters: host, port", ex.getMessage());
+ assertEquals("Missing required parameters: <host>, <port>", ex.getMessage());
+ }
+ }
+ @Test
+ public void testNoMissingRequiredParamErrorWithLabelIfHelpOptionSpecified() {
+ class App {
+ @Parameters(hidden = true) // "hidden": don't show this parameter in usage help message
+ List<String> allParameters; // no "index" attribute: captures _all_ arguments (as Strings)
+
+ @Parameters(index = "0", paramLabel = "HOST") InetAddress host;
+ @Parameters(index = "1", paramLabel = "PORT") int port;
+ @Parameters(index = "2..*", paramLabel = "FILES") File[] files;
+
+ @Option(names = "-?", help = true) boolean help;
+ }
+ CommandLine.populateCommand(new App(), new String[] {"-?"});
+ try {
+ CommandLine.populateCommand(new App(), new String[0]);
+ fail("Should not accept missing mandatory parameter");
+ } catch (MissingParameterException ex) {
+ assertEquals("Missing required parameters: HOST, PORT", ex.getMessage());
}
}
@Test
@@ -999,7 +1033,7 @@ public class CommandLineTest {
CommandLine.populateCommand(requiredField, "arg1", "arg2");
fail("Missing required field should have thrown exception");
} catch (MissingParameterException ex) {
- assertEquals("Missing required option 'required'", ex.getMessage());
+ assertEquals("Missing required option '--required=<required>'", ex.getMessage());
}
}
@Test
@@ -1016,7 +1050,7 @@ public class CommandLineTest {
commandLine.parse("arg1", "arg2");
fail("Missing required field should have thrown exception");
} catch (MissingParameterException ex) {
- assertEquals("Missing required option 'required'", ex.getMessage());
+ assertEquals("Missing required option '--required=<required>'", ex.getMessage());
}
}
@@ -1040,6 +1074,10 @@ public class CommandLineTest {
compact = CommandLine.populateCommand(new CompactFields(), "-vroout");
verifyCompact(compact, true, true, "out", null);
+ // compact group with separator
+ compact = CommandLine.populateCommand(new CompactFields(), "-vro=out");
+ verifyCompact(compact, true, true, "out", null);
+
compact = CommandLine.populateCommand(new CompactFields(), "-rv p1 p2".split(" "));
verifyCompact(compact, true, true, null, fileArray("p1", "p2"));
@@ -1062,15 +1100,16 @@ public class CommandLineTest {
CommandLine.populateCommand(new CompactFields(), "-oout -r -vp1 p2".split(" "));
fail("should fail: -v does not take an argument");
} catch (UnmatchedArgumentException ex) {
- assertEquals("Unmatched arguments [-p1, p2]", ex.getMessage());
+ assertEquals("Unmatched argument [-p1]", ex.getMessage());
}
}
@Test
public void testCompactFieldsWithUnmatchedArguments() {
+ setTraceLevel("OFF");
CommandLine cmd = new CommandLine(new CompactFields()).setUnmatchedArgumentsAllowed(true);
cmd.parse("-oout -r -vp1 p2".split(" "));
- assertEquals(Arrays.asList("-p1", "p2"), cmd.getUnmatchedArguments());
+ assertEquals(Arrays.asList("-p1"), cmd.getUnmatchedArguments());
}
@Test
@@ -1101,9 +1140,9 @@ public class CommandLineTest {
}
@Test
- public void testOptionsAfterParamAreInterpretedAsParameters() {
+ public void testOptionsMixedWithParameters() {
CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-r -v p1 -o out p2".split(" "));
- verifyCompact(compact, true, true, null, fileArray("p1", "-o", "out", "p2"));
+ verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
}
@Test
public void testShortOptionsWithSeparatorButNoValueAssignsEmptyStringEvenIfNotLast() {
@@ -1124,13 +1163,60 @@ public class CommandLineTest {
verifyCompact(compact, true, true, "", null);
}
-
@Test
public void testDoubleDashSeparatesPositionalParameters() {
CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-oout -- -r -v p1 p2".split(" "));
verifyCompact(compact, false, false, "out", fileArray("-r", "-v", "p1", "p2"));
}
+ @Test
+ public void testDebugOutputForDoubleDashSeparatesPositionalParameters() throws UnsupportedEncodingException {
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2500);
+ System.setErr(new PrintStream(baos));
+ final String PROPERTY = "picocli.trace";
+ String old = System.getProperty(PROPERTY);
+ System.setProperty(PROPERTY, "DEBUG");
+ CommandLine.populateCommand(new CompactFields(), "-oout -- -r -v p1 p2".split(" "));
+ System.setErr(originalErr);
+ if (old == null) {
+ System.clearProperty(PROPERTY);
+ } else {
+ System.setProperty(PROPERTY, old);
+ }
+ String expected = String.format("" +
+ "[picocli INFO] Parsing 6 command line args [-oout, --, -r, -v, p1, p2]%n" +
+ "[picocli DEBUG] Initializing %1$s$CompactFields: 3 options, 1 positional parameters, 0 required, 0 subcommands.%n" +
+ "[picocli DEBUG] Processing argument '-oout'. Remainder=[--, -r, -v, p1, p2]%n" +
+ "[picocli DEBUG] '-oout' cannot be separated into <option>=<option-parameter>%n" +
+ "[picocli DEBUG] Trying to process '-oout' as clustered short options%n" +
+ "[picocli DEBUG] Found option '-o' in -oout: field java.io.File %1$s$CompactFields.outputFile, arity=1%n" +
+ "[picocli DEBUG] Trying to process 'out' as option parameter%n" +
+ "[picocli INFO] Setting File field 'CompactFields.outputFile' to 'out' (was 'null') for option -o%n" +
+ "[picocli DEBUG] Processing argument '--'. Remainder=[-r, -v, p1, p2]%n" +
+ "[picocli INFO] Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=0. Remainder=[-r, -v, p1, p2]%n" +
+ "[picocli DEBUG] Position 0 is in index range 0..*. Trying to assign args to java.io.File[] %1$s$CompactFields.inputFiles, arity=0..1%n" +
+ "[picocli INFO] Adding [-r] to File[] field 'CompactFields.inputFiles' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 1.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=1. Remainder=[-v, p1, p2]%n" +
+ "[picocli DEBUG] Position 1 is in index range 0..*. Trying to assign args to java.io.File[] %1$s$CompactFields.inputFiles, arity=0..1%n" +
+ "[picocli INFO] Adding [-v] to File[] field 'CompactFields.inputFiles' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 2.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=2. Remainder=[p1, p2]%n" +
+ "[picocli DEBUG] Position 2 is in index range 0..*. Trying to assign args to java.io.File[] %1$s$CompactFields.inputFiles, arity=0..1%n" +
+ "[picocli INFO] Adding [p1] to File[] field 'CompactFields.inputFiles' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 3.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=3. Remainder=[p2]%n" +
+ "[picocli DEBUG] Position 3 is in index range 0..*. Trying to assign args to java.io.File[] %1$s$CompactFields.inputFiles, arity=0..1%n" +
+ "[picocli INFO] Adding [p2] to File[] field 'CompactFields.inputFiles' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 4.%n",
+ CommandLineTest.class.getName(), new File("/home/rpopma/picocli"));
+ String actual = new String(baos.toByteArray(), "UTF8");
+ //System.out.println(actual);
+ assertEquals(expected, actual);
+ }
+
private File[] fileArray(final String ... paths) {
File[] result = new File[paths.length];
for (int i = 0; i < result.length; i++) {
@@ -1203,25 +1289,25 @@ public class CommandLineTest {
assertEquals("3", arity.toString());
}
@Test
- public void testArityForOption_listFieldImplicitArity0_n() throws Exception {
+ public void testArityForOption_listFieldImplicitArity1() throws Exception {
class ImplicitList { @Option(names = "-a") List<Integer> listIntegers; }
Range arity = Range.optionArity(ImplicitList.class.getDeclaredField("listIntegers"));
- assertEquals(Range.valueOf("0..*"), arity);
- assertEquals("0..*", arity.toString());
+ assertEquals(Range.valueOf("1"), arity);
+ assertEquals("1", arity.toString());
}
@Test
- public void testArityForOption_arrayFieldImplicitArity0_n() throws Exception {
+ public void testArityForOption_arrayFieldImplicitArity1() throws Exception {
class ImplicitList { @Option(names = "-a") int[] intArray; }
Range arity = Range.optionArity(ImplicitList.class.getDeclaredField("intArray"));
- assertEquals(Range.valueOf("0..*"), arity);
- assertEquals("0..*", arity.toString());
+ assertEquals(Range.valueOf("1"), arity);
+ assertEquals("1", arity.toString());
}
@Test
- public void testArityForParameters_booleanFieldImplicitArity0() throws Exception {
+ public void testArityForParameters_booleanFieldImplicitArity1() throws Exception {
class ImplicitBoolField { @Parameters boolean boolSingleValue; }
Range arity = Range.parameterArity(ImplicitBoolField.class.getDeclaredField("boolSingleValue"));
- assertEquals(Range.valueOf("0"), arity);
- assertEquals("0", arity.toString());
+ assertEquals(Range.valueOf("1"), arity);
+ assertEquals("1", arity.toString());
}
@Test
public void testArityForParameters_intFieldImplicitArity1() throws Exception {
@@ -1231,16 +1317,16 @@ public class CommandLineTest {
assertEquals("1", arity.toString());
}
@Test
- public void testArityForParameters_listFieldImplicitArity0_n() throws Exception {
+ public void testArityForParameters_listFieldImplicitArity0_1() throws Exception {
Range arity = Range.parameterArity(ListPositionalParams.class.getDeclaredField("list"));
- assertEquals(Range.valueOf("0..*"), arity);
- assertEquals("0..*", arity.toString());
+ assertEquals(Range.valueOf("0..1"), arity);
+ assertEquals("0..1", arity.toString());
}
@Test
- public void testArityForParameters_arrayFieldImplicitArity0_n() throws Exception {
+ public void testArityForParameters_arrayFieldImplicitArity0_1() throws Exception {
Range arity = Range.parameterArity(CompactFields.class.getDeclaredField("inputFiles"));
- assertEquals(Range.valueOf("0..*"), arity);
- assertEquals("0..*", arity.toString());
+ assertEquals(Range.valueOf("0..1"), arity);
+ assertEquals("0..1", arity.toString());
}
@Test
public void testArrayOptionsWithArity0_nConsumeAllArguments() {
@@ -1411,8 +1497,8 @@ public class CommandLineTest {
BooleanOptionsArity0_nAndParameters
params = CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-bool 123 -other".split(" "));
assertTrue(params.bool);
- assertFalse(params.vOrOther);
- assertArrayEquals(new String[]{ "123", "-other"}, params.params);
+ assertTrue(params.vOrOther);
+ assertArrayEquals(new String[]{ "123"}, params.params);
}
@Test
public void testBooleanOptionsArity0_nFailsIfAttachedParamNotABoolean() { // ignores varargs
@@ -1429,14 +1515,15 @@ public class CommandLineTest {
CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-rv234 -bool".split(" "));
fail("Expected exception");
} catch (UnmatchedArgumentException ok) {
- assertEquals("Unmatched arguments [-234, -bool]", ok.getMessage());
+ assertEquals("Unmatched argument [-234]", ok.getMessage());
}
}
@Test
public void testBooleanOptionsArity0_nShortFormFailsIfAttachedParamNotABooleanWithUnmatchedArgsAllowed() { // ignores varargs
+ setTraceLevel("OFF");
CommandLine cmd = new CommandLine(new BooleanOptionsArity0_nAndParameters()).setUnmatchedArgumentsAllowed(true);
cmd.parse("-rv234 -bool".split(" "));
- assertEquals(Arrays.asList("-234", "-bool"), cmd.getUnmatchedArguments());
+ assertEquals(Arrays.asList("-234"), cmd.getUnmatchedArguments());
}
@Test
public void testBooleanOptionsArity0_nShortFormFailsIfAttachedWithSepParamNotABoolean() { // ignores varargs
@@ -1493,7 +1580,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool".split(" "));
fail("Missing param was accepted for boolean with arity=1");
} catch (ParameterException expected) {
- assertEquals("Missing required parameter for option '-bool' at index 0 (aBoolean)", expected.getMessage());
+ assertEquals("Missing required parameter for option '-bool' at index 0 (<aBoolean>)", expected.getMessage());
}
}
@@ -1545,6 +1632,13 @@ public class CommandLineTest {
assertArrayEquals(Arrays.toString(params.doubleOptions),
new double[] {1.1}, params.doubleOptions, 0.000001);
assertArrayEquals(new double[]{2.2, 3.3, 4.4}, params.doubleParams, 0.000001);
+
+ // repeated occurrence
+ params = CommandLine.populateCommand(new Options1ArityAndParameters(), "-doubles 1.1 -doubles 2.2 -doubles 3.3 4.4".split(" "));
+ assertArrayEquals(Arrays.toString(params.doubleOptions),
+ new double[] {1.1, 2.2, 3.3}, params.doubleOptions, 0.000001);
+ assertArrayEquals(new double[]{4.4}, params.doubleParams, 0.000001);
+
}
private static class ArrayOptionArity2AndParameters {
@@ -1558,6 +1652,12 @@ public class CommandLineTest {
assertArrayEquals(Arrays.toString(params.doubleOptions),
new double[] {1.1, 2.2, }, params.doubleOptions, 0.000001);
assertArrayEquals(new double[]{3.3, 4.4}, params.doubleParams, 0.000001);
+
+ // repeated occurrence
+ params = CommandLine.populateCommand(new ArrayOptionArity2AndParameters(), "-doubles 1.1 2.2 -doubles 3.3 4.4 0".split(" "));
+ assertArrayEquals(Arrays.toString(params.doubleOptions),
+ new double[] {1.1, 2.2, 3.3, 4.4 }, params.doubleOptions, 0.000001);
+ assertArrayEquals(new double[]{ 0.0 }, params.doubleParams, 0.000001);
}
@Test
public void testArrayOptionsWithArity2Consume2ArgumentsEvenIfFirstIsAttached() {
@@ -1566,10 +1666,25 @@ public class CommandLineTest {
assertArrayEquals(Arrays.toString(params.doubleOptions),
new double[] {1.1, 2.2, }, params.doubleOptions, 0.000001);
assertArrayEquals(new double[]{3.3, 4.4}, params.doubleParams, 0.000001);
+
+ // repeated occurrence
+ params = CommandLine.populateCommand(new ArrayOptionArity2AndParameters(), "-doubles=1.1 2.2 -doubles=3.3 4.4 0".split(" "));
+ assertArrayEquals(Arrays.toString(params.doubleOptions),
+ new double[] {1.1, 2.2, 3.3, 4.4}, params.doubleOptions, 0.000001);
+ assertArrayEquals(new double[]{0}, params.doubleParams, 0.000001);
+ }
+ /** Arity should not limit the total number of values put in an array or collection #191 */
+ @Test
+ public void testArrayOptionsWithArity2MayContainMoreThan2Values() {
+ ArrayOptionArity2AndParameters
+ params = CommandLine.populateCommand(new ArrayOptionArity2AndParameters(), "-doubles=1 2 -doubles 3 4 -doubles 5 6".split(" "));
+ assertArrayEquals(Arrays.toString(params.doubleOptions),
+ new double[] {1, 2, 3, 4, 5, 6 }, params.doubleOptions, 0.000001);
+ assertArrayEquals(null, params.doubleParams, 0.000001);
}
@Test
- public void testArrayOptionWithoutArityConsumesAllArguments() {
+ public void testArrayOptionWithoutArityConsumesOneArgument() { // #192
class OptionsNoArityAndParameters {
@Parameters char[] charParams;
@Option(names = "-chars") char[] charOptions;
@@ -1577,8 +1692,21 @@ public class CommandLineTest {
OptionsNoArityAndParameters
params = CommandLine.populateCommand(new OptionsNoArityAndParameters(), "-chars a b c d".split(" "));
assertArrayEquals(Arrays.toString(params.charOptions),
- new char[] {'a', 'b', 'c', 'd'}, params.charOptions);
- assertArrayEquals(Arrays.toString(params.charParams), null, params.charParams);
+ new char[] {'a', }, params.charOptions);
+ assertArrayEquals(Arrays.toString(params.charParams), new char[] {'b', 'c', 'd'}, params.charParams);
+
+ // repeated occurrence
+ params = CommandLine.populateCommand(new OptionsNoArityAndParameters(), "-chars a -chars b c d".split(" "));
+ assertArrayEquals(Arrays.toString(params.charOptions),
+ new char[] {'a', 'b', }, params.charOptions);
+ assertArrayEquals(Arrays.toString(params.charParams), new char[] {'c', 'd'}, params.charParams);
+
+ try {
+ CommandLine.populateCommand(new OptionsNoArityAndParameters(), "-chars".split(" "));
+ fail("expected MissingParameterException");
+ } catch (MissingParameterException ok) {
+ assertEquals("Missing required parameter for option '-chars' (<charOptions>)", ok.getMessage());
+ }
}
@Test(expected = MissingTypeConverterException.class)
@@ -1590,6 +1718,22 @@ public class CommandLineTest {
}
@Test
+ public void testArrayParametersWithDefaultArity() {
+ class ArrayParamsDefaultArity {
+ @Parameters
+ List<String> params;
+ }
+ ArrayParamsDefaultArity params = CommandLine.populateCommand(new ArrayParamsDefaultArity(), "a", "b", "c");
+ assertEquals(Arrays.asList("a", "b", "c"), params.params);
+
+ params = CommandLine.populateCommand(new ArrayParamsDefaultArity(), "a");
+ assertEquals(Arrays.asList("a"), params.params);
+
+ params = CommandLine.populateCommand(new ArrayParamsDefaultArity());
+ assertEquals(null, params.params);
+ }
+
+ @Test
public void testArrayParametersWithArityMinusOneToN() {
class ArrayParamsNegativeArity {
@Parameters(arity = "-1..*")
@@ -1637,7 +1781,7 @@ public class CommandLineTest {
params = CommandLine.populateCommand(new ArrayParamsArity1_n());
fail("Should not accept input with missing parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameters at positions 0..*: params", ex.getMessage());
+ assertEquals("Missing required parameters at positions 0..*: <params>", ex.getMessage());
}
}
@@ -1654,14 +1798,14 @@ public class CommandLineTest {
params = CommandLine.populateCommand(new ArrayParamsArity2_n(), "a");
fail("Should not accept input with missing parameter");
} catch (MissingParameterException ex) {
- assertEquals("positional parameter at index 0..* (params) requires at least 2 values, but only 1 were specified.", ex.getMessage());
+ assertEquals("positional parameter at index 0..* (<params>) requires at least 2 values, but only 1 were specified: [a]", ex.getMessage());
}
try {
params = CommandLine.populateCommand(new ArrayParamsArity2_n());
fail("Should not accept input with missing parameter");
} catch (MissingParameterException ex) {
- assertEquals("positional parameter at index 0..* (params) requires at least 2 values, but only 0 were specified.", ex.getMessage());
+ assertEquals("positional parameter at index 0..* (<params>) requires at least 2 values, but none were specified.", ex.getMessage());
}
}
@@ -1671,13 +1815,19 @@ public class CommandLineTest {
@Parameters(arity = "-1")
List<String> params;
}
- NonVarArgArrayParamsNegativeArity params = CommandLine.populateCommand(new NonVarArgArrayParamsNegativeArity(), "a", "b", "c");
- assertEquals(Arrays.asList(), params.params);
-
- params = CommandLine.populateCommand(new NonVarArgArrayParamsNegativeArity(), "a");
- assertEquals(Arrays.asList(), params.params);
-
- params = CommandLine.populateCommand(new NonVarArgArrayParamsNegativeArity());
+ try {
+ CommandLine.populateCommand(new NonVarArgArrayParamsNegativeArity(), "a", "b", "c");
+ fail("Expected UnmatchedArgumentException");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched arguments [a, b, c]", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new NonVarArgArrayParamsNegativeArity(), "a");
+ fail("Expected UnmatchedArgumentException");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [a]", ex.getMessage());
+ }
+ NonVarArgArrayParamsNegativeArity params = CommandLine.populateCommand(new NonVarArgArrayParamsNegativeArity());
assertEquals(null, params.params);
}
@@ -1687,13 +1837,19 @@ public class CommandLineTest {
@Parameters(arity = "0")
List<String> params;
}
- NonVarArgArrayParamsZeroArity params = CommandLine.populateCommand(new NonVarArgArrayParamsZeroArity(), "a", "b", "c");
- assertEquals(new ArrayList<String>(), params.params);
-
- params = CommandLine.populateCommand(new NonVarArgArrayParamsZeroArity(), "a");
- assertEquals(new ArrayList<String>(), params.params);
-
- params = CommandLine.populateCommand(new NonVarArgArrayParamsZeroArity());
+ try {
+ CommandLine.populateCommand(new NonVarArgArrayParamsZeroArity(), "a", "b", "c");
+ fail("Expected UnmatchedArgumentException");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched arguments [a, b, c]", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new NonVarArgArrayParamsZeroArity(), "a");
+ fail("Expected UnmatchedArgumentException");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [a]", ex.getMessage());
+ }
+ NonVarArgArrayParamsZeroArity params = CommandLine.populateCommand(new NonVarArgArrayParamsZeroArity());
assertEquals(null, params.params);
}
@@ -1703,17 +1859,17 @@ public class CommandLineTest {
@Parameters(arity = "1")
List<String> params;
}
- NonVarArgArrayParamsArity1 params = CommandLine.populateCommand(new NonVarArgArrayParamsArity1(), "a", "b", "c");
- assertEquals(Arrays.asList("a"), params.params);
+ NonVarArgArrayParamsArity1 actual = CommandLine.populateCommand(new NonVarArgArrayParamsArity1(), "a", "b", "c");
+ assertEquals(Arrays.asList("a", "b", "c"), actual.params);
- params = CommandLine.populateCommand(new NonVarArgArrayParamsArity1(), "a");
+ NonVarArgArrayParamsArity1 params = CommandLine.populateCommand(new NonVarArgArrayParamsArity1(), "a");
assertEquals(Arrays.asList("a"), params.params);
try {
params = CommandLine.populateCommand(new NonVarArgArrayParamsArity1());
fail("Should not accept input with missing parameter");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: params", ex.getMessage());
+ assertEquals("Missing required parameter: <params>", ex.getMessage());
}
}
@@ -1723,21 +1879,26 @@ public class CommandLineTest {
@Parameters(arity = "2")
List<String> params;
}
- NonVarArgArrayParamsArity2 params = CommandLine.populateCommand(new NonVarArgArrayParamsArity2(), "a", "b", "c");
- assertEquals(Arrays.asList("a", "b"), params.params);
+ NonVarArgArrayParamsArity2 params = null;
+ try {
+ CommandLine.populateCommand(new NonVarArgArrayParamsArity2(), "a", "b", "c");
+ fail("expected MissingParameterException");
+ } catch (MissingParameterException ex) {
+ assertEquals("positional parameter at index 0..* (<params>) requires at least 2 values, but only 1 were specified: [c]", ex.getMessage());
+ }
try {
params = CommandLine.populateCommand(new NonVarArgArrayParamsArity2(), "a");
fail("Should not accept input with missing parameter");
} catch (MissingParameterException ex) {
- assertEquals("positional parameter at index 0..* (params) requires at least 2 values, but only 1 were specified.", ex.getMessage());
+ assertEquals("positional parameter at index 0..* (<params>) requires at least 2 values, but only 1 were specified: [a]", ex.getMessage());
}
try {
params = CommandLine.populateCommand(new NonVarArgArrayParamsArity2());
fail("Should not accept input with missing parameter");
} catch (MissingParameterException ex) {
- assertEquals("positional parameter at index 0..* (params) requires at least 2 values, but only 0 were specified.", ex.getMessage());
+ assertEquals("positional parameter at index 0..* (<params>) requires at least 2 values, but none were specified.", ex.getMessage());
}
}
@@ -1750,7 +1911,7 @@ public class CommandLineTest {
try {
CommandLine.populateCommand(new WithParams(), new String[0]);
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameters: param0, param1", ex.getMessage());
+ assertEquals("Missing required parameters: <param0>, <param1>", ex.getMessage());
}
}
@@ -1808,8 +1969,8 @@ public class CommandLineTest {
try {
CommandLine.populateCommand(new App(), "--opt=abc");
fail("Expected failure with unknown separator");
- } catch (UnmatchedArgumentException ok) {
- assertEquals("Unmatched argument [--opt=abc]", ok.getMessage());
+ } catch (MissingParameterException ok) {
+ assertEquals("Missing required option '--opt:<opt>'", ok.getMessage());
}
}
@Test
@@ -1821,8 +1982,8 @@ public class CommandLineTest {
try {
CommandLine.populateCommand(new App(), "--opt=abc");
fail("Expected failure with unknown separator");
- } catch (UnmatchedArgumentException ok) {
- assertEquals("Unmatched argument [--opt=abc]", ok.getMessage());
+ } catch (MissingParameterException ok) {
+ assertEquals("Missing required option '--opt:<opt>'", ok.getMessage());
}
}
@Test
@@ -1831,11 +1992,14 @@ public class CommandLineTest {
class App {
@Option(names = "--opt", required = true) String opt;
}
+ setTraceLevel("OFF");
CommandLine cmd = new CommandLine(new App()).setUnmatchedArgumentsAllowed(true);
try {
cmd.parse("--opt=abc");
+ fail("Expected MissingParameterException");
} catch (MissingParameterException ok) {
- assertEquals("Missing required option 'opt'", ok.getMessage());
+ assertEquals("Missing required option '--opt:<opt>'", ok.getMessage());
+ assertEquals(Arrays.asList("--opt=abc"), cmd.getUnmatchedArguments());
}
}
@Test
@@ -1845,13 +2009,13 @@ public class CommandLineTest {
params = CommandLine.populateCommand(new VariousPrefixCharacters(), "--dash".split(" "));
fail("int option needs arg");
} catch (ParameterException ex) {
- assertEquals("Missing required parameter for option '-d' (dash)", ex.getMessage());
+ assertEquals("Missing required parameter for option '-d' (<dash>)", ex.getMessage());
}
try {
params = CommandLine.populateCommand(new VariousPrefixCharacters(), "--owner".split(" "));
} catch (ParameterException ex) {
- assertEquals("Missing required parameter for option '/Owner' (owner)", ex.getMessage());
+ assertEquals("Missing required parameter for option '/Owner' (<owner>)", ex.getMessage());
}
params = CommandLine.populateCommand(new VariousPrefixCharacters(), "--owner=".split(" "));
@@ -1981,14 +2145,21 @@ public class CommandLineTest {
class TextOption {
@CommandLine.Option(names = "-t") String[] text;
}
- TextOption opt = CommandLine.populateCommand(new TextOption(), "-t", "\"a text\"", "\"another text\"", "\"x z\"");
+ TextOption opt = CommandLine.populateCommand(new TextOption(), "-t", "\"a text\"", "-t", "\"another text\"", "-t", "\"x z\"");
assertArrayEquals(new String[]{"a text", "another text", "x z"}, opt.text);
- opt = CommandLine.populateCommand(new TextOption(), "-t\"a text\"", "\"another text\"", "\"x z\"");
+ opt = CommandLine.populateCommand(new TextOption(), "-t\"a text\"", "-t\"another text\"", "-t\"x z\"");
assertArrayEquals(new String[]{"a text", "another text", "x z"}, opt.text);
- opt = CommandLine.populateCommand(new TextOption(), "-t=\"a text\"", "\"another text\"", "\"x z\"");
+ opt = CommandLine.populateCommand(new TextOption(), "-t=\"a text\"", "-t=\"another text\"", "-t=\"x z\"");
assertArrayEquals(new String[]{"a text", "another text", "x z"}, opt.text);
+
+ try {
+ opt = CommandLine.populateCommand(new TextOption(), "-t=\"a text\"", "-t=\"another text\"", "\"x z\"");
+ fail("Expected UnmatchedArgumentException");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched argument [\"x z\"]", ok.getMessage());
+ }
}
@Test
@@ -2088,20 +2259,22 @@ public class CommandLineTest {
CommandLine.populateCommand(new App(), "000");
fail("Should fail with missingParamException");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: file1", ex.getMessage());
+ assertEquals("Missing required parameter: <file1>", ex.getMessage());
}
}
@Test
public void testPositionalParamWithFixedIndexRange() {
+ System.setProperty("picocli.trace", "OFF");
class App {
@Parameters(index = "0..1") File file0_1;
@Parameters(index = "1..2", type = File.class) List<File> fileList1_2;
@Parameters(index = "0..3") File[] fileArray0_3 = new File[4];
@Parameters List<String> all;
}
- App app1 = CommandLine.populateCommand(new App(), "000", "111", "222", "333");
- assertEquals("field initialized with arg[0]", new File("000"), app1.file0_1);
+ App app1 = new App();
+ new CommandLine(app1).setOverwrittenOptionsAllowed(true).parse("000", "111", "222", "333");
+ assertEquals("field initialized with arg[0]", new File("111"), app1.file0_1);
assertEquals("arg[1] and arg[2]", Arrays.asList(
new File("111"),
new File("222")), app1.fileList1_2);
@@ -2113,8 +2286,9 @@ public class CommandLineTest {
new File("333")}, app1.fileArray0_3);
assertEquals("args", Arrays.asList("000", "111", "222", "333"), app1.all);
- App app2 = CommandLine.populateCommand(new App(), "000", "111");
- assertEquals("field initialized with arg[0]", new File("000"), app2.file0_1);
+ App app2 = new App();
+ new CommandLine(app2).setOverwrittenOptionsAllowed(true).parse("000", "111");
+ assertEquals("field initialized with arg[0]", new File("111"), app2.file0_1);
assertEquals("arg[1]", Arrays.asList(new File("111")), app2.fileList1_2);
assertArrayEquals("arg[0-3]", new File[]{
null, null, null, null, // existing values
@@ -2124,7 +2298,7 @@ public class CommandLineTest {
App app3 = CommandLine.populateCommand(new App(), "000");
assertEquals("field initialized with arg[0]", new File("000"), app3.file0_1);
- assertEquals("arg[1]", new ArrayList<File>(), app3.fileList1_2);
+ assertEquals("arg[1]", null, app3.fileList1_2);
assertArrayEquals("arg[0-3]", new File[]{
null, null, null, null, // existing values
new File("000")}, app3.fileArray0_3);
@@ -2134,7 +2308,7 @@ public class CommandLineTest {
CommandLine.populateCommand(new App());
fail("Should fail with missingParamException");
} catch (MissingParameterException ex) {
- assertEquals("Missing required parameter: file0_1", ex.getMessage());
+ assertEquals("Missing required parameter: <file0_1>", ex.getMessage());
}
}
@@ -2152,7 +2326,7 @@ public class CommandLineTest {
assertEquals(1111, app1.port1);
assertEquals(InetAddress.getByName("localhost"), app1.host2);
assertArrayEquals(new int[]{2222, 3333}, app1.port2range);
- assertArrayEquals(new String[]{"3333", "file1", "file2"}, app1.files);
+ assertArrayEquals(new String[]{"file1", "file2"}, app1.files);
}
@Ignore("Requires #70 support for variable arity in positional parameters")
@@ -2247,6 +2421,7 @@ public class CommandLineTest {
class SingleValue {
@Parameters(index = "0") String str;
}
+ setTraceLevel("OFF");
CommandLine cmd = new CommandLine(new SingleValue()).setUnmatchedArgumentsAllowed(true);
cmd.parse("val1", "val2");
assertEquals("val1", ((SingleValue)cmd.getCommand()).str);
@@ -2258,19 +2433,28 @@ public class CommandLineTest {
class SingleValue {
@Parameters(index = "0..2") String[] str;
}
+ setTraceLevel("OFF");
CommandLine cmd = new CommandLine(new SingleValue()).setUnmatchedArgumentsAllowed(true);
cmd.parse("val0", "val1", "val2", "val3");
assertArrayEquals(new String[]{"val0", "val1", "val2"}, ((SingleValue)cmd.getCommand()).str);
assertEquals(Arrays.asList("val3"), cmd.getUnmatchedArguments());
}
- @Test // TODO
+ @Test
public void testPositionalParamSingleValueButWithoutIndex() throws Exception {
class SingleValue {
@Parameters String str;
}
- SingleValue single = CommandLine.populateCommand(new SingleValue(),"val1", "val2");
- assertEquals("val1", single.str);
+ try {
+ CommandLine.populateCommand(new SingleValue(),"val1", "val2");
+ fail("Expected OverwrittenOptionException");
+ } catch (OverwrittenOptionException ex) {
+ assertEquals("positional parameter at index 0..* (<str>) should be specified only once", ex.getMessage());
+ }
+ setTraceLevel("OFF");
+ CommandLine cmd = new CommandLine(new SingleValue()).setOverwrittenOptionsAllowed(true);
+ cmd.parse("val1", "val2");
+ assertEquals("val2", ((SingleValue) cmd.getCommand()).str);
}
@Test
@@ -2281,14 +2465,27 @@ public class CommandLineTest {
Args args = CommandLine.populateCommand(new Args(), "-a=a,b,c");
assertArrayEquals(new String[] {"a", "b", "c"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C");
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "-a=B", "-a", "C");
assertArrayEquals(new String[] {"a", "b", "c", "B", "C"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a", "a,b,c", "B", "C");
+ args = CommandLine.populateCommand(new Args(), "-a", "a,b,c", "-a", "B", "-a", "C");
assertArrayEquals(new String[] {"a", "b", "c", "B", "C"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C", "D,E,F");
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "-a", "B", "-a", "C", "-a", "D,E,F");
assertArrayEquals(new String[] {"a", "b", "c", "B", "C", "D", "E", "F"}, args.values);
+
+ try {
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C");
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched arguments [B, C]", ok.getMessage());
+ }
+ try {
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "-a=C");
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched argument [B]", ok.getMessage());
+ }
}
@Test
@@ -2299,14 +2496,27 @@ public class CommandLineTest {
Args args = CommandLine.populateCommand(new Args(), "-a=\"a b c\"");
assertArrayEquals(new String[] {"a", "b", "c"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a=a b c", "B", "C");
+ args = CommandLine.populateCommand(new Args(), "-a=a b c", "-a", "B", "-a", "C");
assertArrayEquals(new String[] {"a", "b", "c", "B", "C"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a", "\"a b c\"", "B", "C");
+ args = CommandLine.populateCommand(new Args(), "-a", "\"a b c\"", "-a=B", "-a=C");
assertArrayEquals(new String[] {"a", "b", "c", "B", "C"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a=\"a b c\"", "B", "C", "D E F");
+ args = CommandLine.populateCommand(new Args(), "-a=\"a b c\"", "-a=B", "-a", "C", "-a=D E F");
assertArrayEquals(new String[] {"a", "b", "c", "B", "C", "D", "E", "F"}, args.values);
+
+ try {
+ args = CommandLine.populateCommand(new Args(), "-a=a b c", "B", "C");
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched arguments [B, C]", ok.getMessage());
+ }
+ try {
+ args = CommandLine.populateCommand(new Args(), "-a=a b c", "B", "-a=C");
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched argument [B]", ok.getMessage());
+ }
}
@Test
@@ -2315,24 +2525,28 @@ public class CommandLineTest {
@Option(names = "-a", split = ",", arity = "0..4") String[] values;
@Parameters() String[] params;
}
- Args args = CommandLine.populateCommand(new Args(), "-a=a,b,c");
+ Args args = CommandLine.populateCommand(new Args(), "-a=a,b,c"); // 1 args
assertArrayEquals(new String[] {"a", "b", "c"}, args.values);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C");
- assertArrayEquals(new String[] {"a", "b", "c", "B"}, args.values);
- assertArrayEquals(new String[] {"C"}, args.params);
+ args = CommandLine.populateCommand(new Args(), "-a"); // 0 args
+ assertArrayEquals(new String[0], args.values);
+ assertNull(args.params);
- args = CommandLine.populateCommand(new Args(), "-a", "a,b,c", "B", "C");
- assertArrayEquals(new String[] {"a", "b", "c", "B"}, args.values);
- assertArrayEquals(new String[] {"C"}, args.params);
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C"); // 3 args
+ assertArrayEquals(new String[] {"a", "b", "c", "B", "C"}, args.values);
+ assertNull(args.params);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C", "D,E,F");
- assertArrayEquals(new String[] {"a", "b", "c", "B"}, args.values);
- assertArrayEquals(new String[] {"C", "D,E,F"}, args.params);
+ args = CommandLine.populateCommand(new Args(), "-a", "a,b,c", "B", "C"); // 3 args
+ assertArrayEquals(new String[] {"a", "b", "c", "B", "C"}, args.values);
+ assertNull(args.params);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c,d,e", "B", "C", "D,E,F");
- assertArrayEquals(new String[] {"a", "b", "c", "d"}, args.values);
- assertArrayEquals(new String[] {"e", "B", "C", "D,E,F"}, args.params);
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C", "D,E,F"); // 4 args
+ assertArrayEquals(new String[] {"a", "b", "c", "B", "C", "D", "E", "F"}, args.values);
+ assertNull(args.params);
+
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c,d", "B", "C", "D", "E,F"); // 5 args
+ assertArrayEquals(new String[] {"a", "b", "c", "d", "B", "C", "D"}, args.values);
+ assertArrayEquals(new String[] {"E,F"}, args.params);
}
@Test
@@ -2343,14 +2557,21 @@ public class CommandLineTest {
Args args = CommandLine.populateCommand(new Args(), "-a=a,b,c");
assertEquals(Arrays.asList("a", "b", "c"), args.values);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C");
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "-a", "B", "-a=C");
assertEquals(Arrays.asList("a", "b", "c", "B", "C"), args.values);
- args = CommandLine.populateCommand(new Args(), "-a", "a,b,c", "B", "C");
+ args = CommandLine.populateCommand(new Args(), "-a", "a,b,c", "-a", "B", "-a", "C");
assertEquals(Arrays.asList("a", "b", "c", "B", "C"), args.values);
- args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C", "D,E,F");
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "-a", "B", "-a", "C", "-a", "D,E,F");
assertEquals(Arrays.asList("a", "b", "c", "B", "C", "D", "E", "F"), args.values);
+
+ try {
+ args = CommandLine.populateCommand(new Args(), "-a=a,b,c", "B", "C");
+ fail("Expected UnmatchedArgumentException");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched arguments [B, C]", ok.getMessage());
+ }
}
@Test
@@ -2376,17 +2597,32 @@ public class CommandLineTest {
class Args {
@Parameters(arity = "2..4", split = ",") String[] values;
}
- Args args = CommandLine.populateCommand(new Args(), "a,b,c");
- assertArrayEquals(new String[] {"a", "b", "c"}, args.values);
-
- args = CommandLine.populateCommand(new Args(), "a,b,c", "B", "C");
- assertArrayEquals(new String[] {"a", "b", "c", "B"}, args.values);
+ Args args = CommandLine.populateCommand(new Args(), "a,b", "c,d"); // 2 args
+ assertArrayEquals(new String[] {"a", "b", "c", "d"}, args.values);
- args = CommandLine.populateCommand(new Args(), "a,b,c", "B,C");
- assertArrayEquals(new String[] {"a", "b", "c", "B"}, args.values);
+ args = CommandLine.populateCommand(new Args(), "a,b", "c,d", "e,f"); // 3 args
+ assertArrayEquals(new String[] {"a", "b", "c", "d", "e", "f"}, args.values);
- args = CommandLine.populateCommand(new Args(), "a,b", "A,B,C");
- assertArrayEquals(new String[] {"a", "b", "A", "B"}, args.values);
+ args = CommandLine.populateCommand(new Args(), "a,b,c", "B", "d", "e,f"); // 4 args
+ assertArrayEquals(new String[] {"a", "b", "c", "B", "d", "e", "f"}, args.values);
+ try {
+ CommandLine.populateCommand(new Args(), "a,b,c,d,e"); // 1 arg: should fail
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("positional parameter at index 0..* (<values>) requires at least 2 values, but only 1 were specified: [a,b,c,d,e]", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new Args()); // 0 arg: should fail
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("positional parameter at index 0..* (<values>) requires at least 2 values, but none were specified.", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new Args(), "a,b,c", "B,C", "d", "e", "f,g"); // 5 args
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("positional parameter at index 0..* (<values>) requires at least 2 values, but only 1 were specified: [f,g]", ex.getMessage());
+ }
}
@Test
@@ -2444,6 +2680,123 @@ public class CommandLineTest {
assertFalse("NOT status --showIgnored", status.showIgnored);
assertEquals("status -u=no", Demo.GitStatusMode.no, status.mode);
}
+ @Test
+ public void testTracingInfoWithSubCommands() throws Exception {
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2500);
+ System.setErr(new PrintStream(baos));
+ final String PROPERTY = "picocli.trace";
+ String old = System.getProperty(PROPERTY);
+ System.setProperty(PROPERTY, "");
+ CommandLine commandLine = Demo.mainCommand();
+ commandLine.parse("--git-dir=/home/rpopma/picocli", "commit", "-m", "\"Fixed typos\"", "--", "src1.java", "src2.java", "src3.java");
+ System.setErr(originalErr);
+ if (old == null) {
+ System.clearProperty(PROPERTY);
+ } else {
+ System.setProperty(PROPERTY, old);
+ }
+ String expected = String.format("" +
+ "[picocli INFO] Parsing 8 command line args [--git-dir=/home/rpopma/picocli, commit, -m, \"Fixed typos\", --, src1.java, src2.java, src3.java]%n" +
+ "[picocli INFO] Setting File field 'Git.gitDir' to '%s' (was 'null') for option --git-dir%n" +
+ "[picocli INFO] Adding [Fixed typos] to List<String> field 'GitCommit.message' for option -m%n" +
+ "[picocli INFO] Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n" +
+ "[picocli INFO] Adding [src1.java] to List<File> field 'GitCommit.files' for args[0..*]%n" +
+ "[picocli INFO] Adding [src2.java] to List<File> field 'GitCommit.files' for args[0..*]%n" +
+ "[picocli INFO] Adding [src3.java] to List<File> field 'GitCommit.files' for args[0..*]%n",
+ new File("/home/rpopma/picocli"));
+ String actual = new String(baos.toByteArray(), "UTF8");
+ //System.out.println(actual);
+ assertEquals(expected, actual);
+ }
+ @Test
+ public void testTracingDebugWithSubCommands() throws Exception {
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2500);
+ System.setErr(new PrintStream(baos));
+ final String PROPERTY = "picocli.trace";
+ String old = System.getProperty(PROPERTY);
+ System.setProperty(PROPERTY, "DEBUG");
+ CommandLine commandLine = Demo.mainCommand();
+ commandLine.parse("--git-dir=/home/rpopma/picocli", "commit", "-m", "\"Fixed typos\"", "--", "src1.java", "src2.java", "src3.java");
+ System.setErr(originalErr);
+ if (old == null) {
+ System.clearProperty(PROPERTY);
+ } else {
+ System.setProperty(PROPERTY, old);
+ }
+ String expected = String.format("" +
+ "[picocli INFO] Parsing 8 command line args [--git-dir=/home/rpopma/picocli, commit, -m, \"Fixed typos\", --, src1.java, src2.java, src3.java]%n" +
+ "[picocli DEBUG] Initializing %1$s$Git: 3 options, 0 positional parameters, 0 required, 11 subcommands.%n" +
+ "[picocli DEBUG] Processing argument '--git-dir=/home/rpopma/picocli'. Remainder=[commit, -m, \"Fixed typos\", --, src1.java, src2.java, src3.java]%n" +
+ "[picocli DEBUG] Separated '--git-dir' option from '/home/rpopma/picocli' option parameter%n" +
+ "[picocli DEBUG] Found option named '--git-dir': field java.io.File %1$s$Git.gitDir, arity=1%n" +
+ "[picocli INFO] Setting File field 'Git.gitDir' to '%2$s' (was 'null') for option --git-dir%n" +
+ "[picocli DEBUG] Processing argument 'commit'. Remainder=[-m, \"Fixed typos\", --, src1.java, src2.java, src3.java]%n" +
+ "[picocli DEBUG] Found subcommand 'commit' (%1$s$GitCommit)%n" +
+ "[picocli DEBUG] Initializing %1$s$GitCommit: 8 options, 1 positional parameters, 0 required, 0 subcommands.%n" +
+ "[picocli DEBUG] Processing argument '-m'. Remainder=[\"Fixed typos\", --, src1.java, src2.java, src3.java]%n" +
+ "[picocli DEBUG] '-m' cannot be separated into <option>=<option-parameter>%n" +
+ "[picocli DEBUG] Found option named '-m': field java.util.List %1$s$GitCommit.message, arity=1%n" +
+ "[picocli INFO] Adding [Fixed typos] to List<String> field 'GitCommit.message' for option -m%n" +
+ "[picocli DEBUG] Processing argument '--'. Remainder=[src1.java, src2.java, src3.java]%n" +
+ "[picocli INFO] Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=0. Remainder=[src1.java, src2.java, src3.java]%n" +
+ "[picocli DEBUG] Position 0 is in index range 0..*. Trying to assign args to java.util.List %1$s$GitCommit.files, arity=0..1%n" +
+ "[picocli INFO] Adding [src1.java] to List<File> field 'GitCommit.files' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 1.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=1. Remainder=[src2.java, src3.java]%n" +
+ "[picocli DEBUG] Position 1 is in index range 0..*. Trying to assign args to java.util.List %1$s$GitCommit.files, arity=0..1%n" +
+ "[picocli INFO] Adding [src2.java] to List<File> field 'GitCommit.files' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 2.%n" +
+ "[picocli DEBUG] Processing next arg as a positional parameter at index=2. Remainder=[src3.java]%n" +
+ "[picocli DEBUG] Position 2 is in index range 0..*. Trying to assign args to java.util.List %1$s$GitCommit.files, arity=0..1%n" +
+ "[picocli INFO] Adding [src3.java] to List<File> field 'GitCommit.files' for args[0..*]%n" +
+ "[picocli DEBUG] Consumed 1 arguments, moving position to index 3.%n",
+ Demo.class.getName(), new File("/home/rpopma/picocli"));
+ String actual = new String(baos.toByteArray(), "UTF8");
+ //System.out.println(actual);
+ assertEquals(expected, actual);
+ }
+ @Test
+ public void testTraceWarningIfOptionOverwrittenWhenOverwrittenOptionsAllowed() throws Exception {
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2500);
+ System.setErr(new PrintStream(baos));
+
+ CommandLine cmd = new CommandLine(new PrivateFinalOptionFields()).setOverwrittenOptionsAllowed(true);
+ cmd.parse("-f", "111", "-f", "222", "-f", "333");
+ PrivateFinalOptionFields ff = (PrivateFinalOptionFields) cmd.getCommand();
+ assertEquals("333", ff.field);
+ System.setErr(originalErr);
+
+ String expected = String.format("" +
+ "[picocli WARN] Overwriting String field 'PrivateFinalOptionFields.field' value '111' with '222' for option -f%n" +
+ "[picocli WARN] Overwriting String field 'PrivateFinalOptionFields.field' value '222' with '333' for option -f%n"
+ );
+ String actual = new String(baos.toByteArray(), "UTF8");
+ //System.out.println(actual);
+ assertEquals(expected, actual);
+ }
+ @Test
+ public void testTraceWarningIfUnmatchedArgsWhenUnmatchedArgumentsAllowed() throws Exception {
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2500);
+ System.setErr(new PrintStream(baos));
+
+ class App {
+ @Parameters(index = "0", arity = "2", split = "\\|", type = {Integer.class, String.class})
+ Map<Integer,String> message;
+ }
+ CommandLine cmd = new CommandLine(new App()).setUnmatchedArgumentsAllowed(true).parse("1=a", "2=b", "3=c", "4=d").get(0);
+ assertEquals(Arrays.asList("3=c", "4=d"), cmd.getUnmatchedArguments());
+ System.setErr(originalErr);
+
+ String expected = String.format("[picocli WARN] Unmatched arguments: [3=c, 4=d]%n");
+ String actual = new String(baos.toByteArray(), "UTF8");
+ //System.out.println(actual);
+ assertEquals(expected, actual);
+ }
@Test
public void testCommandListReturnsRegisteredCommands() {
@@ -2554,7 +2907,7 @@ public class CommandLineTest {
createNestedCommand().parse("-a", "-b", "cmd1");
fail("unmatched option should prevents remainder to be parsed as command");
} catch (UnmatchedArgumentException ex) {
- assertEquals("Unmatched arguments [-b, cmd1]", ex.getMessage());
+ assertEquals("Unmatched argument [-b]", ex.getMessage());
}
try {
createNestedCommand().parse("cmd1", "sub21");
@@ -2590,9 +2943,10 @@ public class CommandLineTest {
@Test
public void testParseNestedSubCommandsAllowingUnmatchedArguments() {
+ setTraceLevel("OFF");
List<CommandLine> result1 = createNestedCommand().setUnmatchedArgumentsAllowed(true)
.parse("-a", "-b", "cmd1");
- assertEquals(Arrays.asList("-b", "cmd1"), result1.get(0).getUnmatchedArguments());
+ assertEquals(Arrays.asList("-b"), result1.get(0).getUnmatchedArguments());
List<CommandLine> result2 = createNestedCommand().setUnmatchedArgumentsAllowed(true)
.parse("cmd1", "sub21");
@@ -2680,7 +3034,7 @@ public class CommandLineTest {
" -number=<number>%n"), result);
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = InitializationException.class)
public void testRunRequiresAnnotatedCommand() {
class App implements Runnable {
public void run() { }
@@ -2688,31 +3042,67 @@ public class CommandLineTest {
CommandLine.run(new App(), System.err);
}
- @Test(expected = IllegalArgumentException.class)
+ @Test
+ public void testCallReturnsCallableResultParseSucceeds() throws Exception {
+ @Command class App implements Callable<Boolean> {
+ public Boolean call() { return true; }
+ }
+ assertTrue(CommandLine.call(new App(), System.err));
+ }
+
+ @Test
+ public void testCallReturnsNullAndPrintsErrorIfParseFails() throws Exception {
+ class App implements Callable<Boolean> {
+ @Option(names = "-number") int number;
+ public Boolean call() { return true; }
+ }
+ PrintStream oldErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ System.setErr(new PrintStream(baos, true, "UTF8"));
+ Boolean callResult = CommandLine.call(new App(), System.err, "-number", "not a number");
+ System.setErr(oldErr);
+
+ String result = baos.toString("UTF8");
+ assertNull(callResult);
+ assertEquals(String.format(
+ "Could not convert 'not a number' to int for option '-number': java.lang.NumberFormatException: For input string: \"not a number\"%n" +
+ "Usage: <main class> [-number=<number>]%n" +
+ " -number=<number>%n"), result);
+ }
+
+ @Test(expected = InitializationException.class)
+ public void testCallRequiresAnnotatedCommand() throws Exception {
+ class App implements Callable<Object> {
+ public Object call() { return null; }
+ }
+ CommandLine.call(new App(), System.err);
+ }
+
+ @Test(expected = InitializationException.class)
public void testPopulateCommandRequiresAnnotatedCommand() {
class App { }
CommandLine.populateCommand(new App());
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = InitializationException.class)
public void testUsageObjectPrintstreamRequiresAnnotatedCommand() {
class App { }
CommandLine.usage(new App(), System.out);
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = InitializationException.class)
public void testUsageObjectPrintstreamAnsiRequiresAnnotatedCommand() {
class App { }
CommandLine.usage(new App(), System.out, Help.Ansi.OFF);
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = InitializationException.class)
public void testUsageObjectPrintstreamColorschemeRequiresAnnotatedCommand() {
class App { }
CommandLine.usage(new App(), System.out, Help.defaultColorScheme(Help.Ansi.OFF));
}
- @Test(expected = IllegalArgumentException.class)
+ @Test(expected = InitializationException.class)
public void testConstructorRequiresAnnotatedCommand() {
class App { }
new CommandLine(new App());
@@ -2728,13 +3118,13 @@ public class CommandLineTest {
CommandLine.populateCommand(new App(), "-s", "1", "-s", "2");
fail("expected exception");
} catch (OverwrittenOptionException ex) {
- assertEquals("option '-s' (string) should be specified only once", ex.getMessage());
+ assertEquals("option '-s' (<string>) should be specified only once", ex.getMessage());
}
try {
CommandLine.populateCommand(new App(), "-v", "-v");
fail("expected exception");
} catch (OverwrittenOptionException ex) {
- assertEquals("option '-v' (bool) should be specified only once", ex.getMessage());
+ assertEquals("option '-v' (<bool>) should be specified only once", ex.getMessage());
}
}
@@ -2748,13 +3138,13 @@ public class CommandLineTest {
CommandLine.populateCommand(new App(), "-s", "1", "--str", "2");
fail("expected exception");
} catch (OverwrittenOptionException ex) {
- assertEquals("option '-s' (string) should be specified only once", ex.getMessage());
+ assertEquals("option '-s' (<string>) should be specified only once", ex.getMessage());
}
try {
CommandLine.populateCommand(new App(), "-v", "--verbose");
fail("expected exception");
} catch (OverwrittenOptionException ex) {
- assertEquals("option '-v' (bool) should be specified only once", ex.getMessage());
+ assertEquals("option '-v' (<bool>) should be specified only once", ex.getMessage());
}
}
@@ -2764,6 +3154,7 @@ public class CommandLineTest {
@Option(names = {"-s", "--str"}) String string;
@Option(names = {"-v", "--verbose"}) boolean bool;
}
+ setTraceLevel("OFF");
CommandLine commandLine = new CommandLine(new App()).setOverwrittenOptionsAllowed(true);
commandLine.parse("-s", "1", "--str", "2");
assertEquals("2", ((App) commandLine.getCommand()).string);
@@ -2788,7 +3179,7 @@ public class CommandLineTest {
commandLine.parse("-u", "foo");
fail("expected exception");
} catch (MissingParameterException ex) {
- assertEquals("Missing required option 'password'", ex.getLocalizedMessage());
+ assertEquals("Missing required option '-p=<password>'", ex.getLocalizedMessage());
}
commandLine.parse("-u", "foo", "-p", "abc");
}
@@ -2891,7 +3282,7 @@ public class CommandLineTest {
@Command(subcommands = {ABC.class}) class MainCommand {}
try {
new CommandLine(new MainCommand());
- } catch (IllegalArgumentException ex) {
+ } catch (InitializationException ex) {
String expected = String.format("Cannot instantiate subcommand %s: the class has no constructor", ABC.class.getName());
assertEquals(expected, ex.getMessage());
}
@@ -2902,7 +3293,7 @@ public class CommandLineTest {
@Command(subcommands = {MissingCommandAnnotation.class}) class MainCommand {}
try {
new CommandLine(new MainCommand());
- } catch (IllegalArgumentException ex) {
+ } catch (InitializationException ex) {
String expected = String.format("Subcommand %s is missing the mandatory @Command annotation with a 'name' attribute", MissingCommandAnnotation.class.getName());
assertEquals(expected, ex.getMessage());
}
@@ -2913,9 +3304,645 @@ public class CommandLineTest {
@Command(subcommands = {MissingNameAttribute.class}) class MainCommand {}
try {
new CommandLine(new MainCommand());
- } catch (IllegalArgumentException ex) {
+ } catch (InitializationException ex) {
String expected = String.format("Subcommand %s is missing the mandatory @Command annotation with a 'name' attribute", MissingNameAttribute.class.getName());
assertEquals(expected, ex.getMessage());
}
}
+ @Test
+ public void testMapFieldHappyCase() {
+ class App {
+ @Option(names = {"-P", "-map"}, type = {String.class, String.class}) Map<String, String> map = new HashMap<String, String>();
+ private void validateMapField() {
+ assertEquals(1, map.size());
+ assertEquals(HashMap.class, map.getClass());
+ assertEquals("BBB", map.get("AAA"));
+ }
+ }
+ CommandLine.populateCommand(new App(), "-map", "AAA=BBB").validateMapField();
+ CommandLine.populateCommand(new App(), "-map=AAA=BBB").validateMapField();
+ CommandLine.populateCommand(new App(), "-P=AAA=BBB").validateMapField();
+ CommandLine.populateCommand(new App(), "-PAAA=BBB").validateMapField();
+ CommandLine.populateCommand(new App(), "-P", "AAA=BBB").validateMapField();
+ }
+ @Test
+ public void testMapFieldHappyCaseWithMultipleValues() {
+ class App {
+ @Option(names = {"-P", "-map"}, split = ",", type = {String.class, String.class}) Map<String, String> map;
+ private void validateMapField3Values() {
+ assertEquals(3, map.size());
+ assertEquals(LinkedHashMap.class, map.getClass());
+ assertEquals("BBB", map.get("AAA"));
+ assertEquals("DDD", map.get("CCC"));
+ assertEquals("FFF", map.get("EEE"));
+ }
+ }
+ CommandLine.populateCommand(new App(), "-map=AAA=BBB,CCC=DDD,EEE=FFF").validateMapField3Values();
+ CommandLine.populateCommand(new App(), "-PAAA=BBB,CCC=DDD,EEE=FFF").validateMapField3Values();
+ CommandLine.populateCommand(new App(), "-P", "AAA=BBB,CCC=DDD,EEE=FFF").validateMapField3Values();
+ CommandLine.populateCommand(new App(), "-map=AAA=BBB", "-map=CCC=DDD", "-map=EEE=FFF").validateMapField3Values();
+ CommandLine.populateCommand(new App(), "-PAAA=BBB", "-PCCC=DDD", "-PEEE=FFF").validateMapField3Values();
+ CommandLine.populateCommand(new App(), "-P", "AAA=BBB", "-P", "CCC=DDD", "-P", "EEE=FFF").validateMapField3Values();
+
+ try {
+ CommandLine.populateCommand(new App(), "-P", "AAA=BBB", "CCC=DDD", "EEE=FFF").validateMapField3Values();
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched arguments [CCC=DDD, EEE=FFF]", ok.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-map=AAA=BBB", "CCC=DDD", "EEE=FFF").validateMapField3Values();
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched arguments [CCC=DDD, EEE=FFF]", ok.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-PAAA=BBB", "-PCCC=DDD", "EEE=FFF").validateMapField3Values();
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched argument [EEE=FFF]", ok.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-P", "AAA=BBB", "-P", "CCC=DDD", "EEE=FFF").validateMapField3Values();
+ fail("Expected UnmatchedArgEx");
+ } catch (UnmatchedArgumentException ok) {
+ assertEquals("Unmatched argument [EEE=FFF]", ok.getMessage());
+ }
+ }
+
+ @Test
+ public void testMapField_InstantiatesConcreteMap() {
+ class App {
+ @Option(names = "-map", type = {String.class, String.class}) TreeMap<String, String> map;
+ }
+ App app = CommandLine.populateCommand(new App(), "-map=AAA=BBB");
+ assertEquals(1, app.map.size());
+ assertEquals(TreeMap.class, app.map.getClass());
+ assertEquals("BBB", app.map.get("AAA"));
+ }
+ @Test
+ public void testMapFieldMissingTypeAttribute() {
+ class App {
+ @Option(names = "-map") TreeMap<String, String> map;
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-map=AAA=BBB");
+ } catch (ParameterException ex) {
+ assertEquals("Field java.util.TreeMap " + App.class.getName() +
+ ".map needs two types (one for the map key, one for the value) but only has 1 types configured.", ex.getMessage());
+ }
+ }
+ @Test
+ public void testMapFieldMissingTypeConverter() {
+ class App {
+ @Option(names = "-map", type = {Thread.class, Thread.class}) TreeMap<String, String> map;
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-map=AAA=BBB");
+ } catch (ParameterException ex) {
+ assertEquals("No TypeConverter registered for java.lang.Thread of field java.util.TreeMap " +
+ App.class.getName() + ".map", ex.getMessage());
+ }
+ }
+ @Test
+ public void testMapFieldWithSplitRegex() {
+ class App {
+ @Option(names = "-fix", split = "\\|", type = {Integer.class, String.class})
+ Map<Integer,String> message;
+ private void validate() {
+ assertEquals(10, message.size());
+ assertEquals(LinkedHashMap.class, message.getClass());
+ assertEquals("FIX.4.4", message.get(8));
+ assertEquals("69", message.get(9));
+ assertEquals("A", message.get(35));
+ assertEquals("MBT", message.get(49));
+ assertEquals("TargetCompID", message.get(56));
+ assertEquals("9", message.get(34));
+ assertEquals("20130625-04:05:32.682", message.get(52));
+ assertEquals("0", message.get(98));
+ assertEquals("30", message.get(108));
+ assertEquals("052", message.get(10));
+ }
+ }
+ CommandLine.populateCommand(new App(), "-fix", "8=FIX.4.4|9=69|35=A|49=MBT|56=TargetCompID|34=9|52=20130625-04:05:32.682|98=0|108=30|10=052").validate();
+ }
+ @Test
+ public void testMapFieldArityWithSplitRegex() {
+ class App {
+ @Option(names = "-fix", arity = "2", split = "\\|", type = {Integer.class, String.class})
+ Map<Integer,String> message;
+ private void validate() {
+ assertEquals(message.toString(), 4, message.size());
+ assertEquals(LinkedHashMap.class, message.getClass());
+ assertEquals("a", message.get(1));
+ assertEquals("b", message.get(2));
+ assertEquals("c", message.get(3));
+ assertEquals("d", message.get(4));
+ }
+ }
+ CommandLine.populateCommand(new App(), "-fix", "1=a", "2=b|3=c|4=d").validate(); // 2 args
+ //Arity should not limit the total number of values put in an array or collection #191
+ CommandLine.populateCommand(new App(), "-fix", "1=a", "2=b", "-fix", "3=c", "4=d").validate(); // 2 args
+
+ try {
+ CommandLine.populateCommand(new App(), "-fix", "1=a|2=b|3=c|4=d"); // 1 arg
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("option '-fix' at index 0 (<Integer=String>) requires at least 2 values, but only 1 were specified: [1=a|2=b|3=c|4=d]", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-fix", "1=a", "2=b", "3=c|4=d"); // 3 args
+ fail("UnmatchedArgumentException expected");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [3=c|4=d]", ex.getMessage());
+ }
+ }
+ @Test
+ public void testMapPositionalParameterFieldMaxArity() {
+ class App {
+ @Parameters(index = "0", arity = "2", split = "\\|", type = {Integer.class, String.class})
+ Map<Integer,String> message;
+ }
+ try {
+ CommandLine.populateCommand(new App(), "1=a", "2=b", "3=c", "4=d");
+ fail("UnmatchedArgumentsException expected");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched arguments [3=c, 4=d]", ex.getMessage());
+ }
+ setTraceLevel("OFF");
+ CommandLine cmd = new CommandLine(new App()).setUnmatchedArgumentsAllowed(true);
+ cmd.parse("1=a", "2=b", "3=c", "4=d");
+ assertEquals(Arrays.asList("3=c", "4=d"), cmd.getUnmatchedArguments());
+ }
+ @Test
+ public void testMapPositionalParameterFieldArity3() {
+ class App {
+ @Parameters(index = "0", arity = "3", split = "\\|", type = {Integer.class, String.class})
+ Map<Integer,String> message;
+ }
+ try {
+ CommandLine.populateCommand(new App(), "1=a", "2=b", "3=c", "4=d");
+ fail("UnmatchedArgumentsException expected");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [4=d]", ex.getMessage());
+ }
+ setTraceLevel("OFF");
+ CommandLine cmd = new CommandLine(new App()).setUnmatchedArgumentsAllowed(true);
+ cmd.parse("1=a", "2=b", "3=c", "4=d");
+ assertEquals(Arrays.asList("4=d"), cmd.getUnmatchedArguments());
+ }
+ @Test
+ public void testMapAndCollectionFieldTypeInference() {
+ class App {
+ @Option(names = "-a") Map<Integer, URI> a;
+ @Option(names = "-b") Map<TimeUnit, StringBuilder> b;
+ @SuppressWarnings("unchecked")
+ @Option(names = "-c") Map c;
+ @Option(names = "-d") List<File> d;
+ @Option(names = "-e") Map<? extends Integer, ? super Long> e;
+ @Option(names = "-f", type = {Long.class, Float.class}) Map<? extends Number, ? super Number> f;
+ @SuppressWarnings("unchecked")
+ @Option(names = "-g", type = {TimeUnit.class, Float.class}) Map g;
+ }
+ App app = CommandLine.populateCommand(new App(),
+ "-a", "8=/path", "-a", "98765432=/path/to/resource",
+ "-b", "SECONDS=abc",
+ "-c", "123=ABC",
+ "-d", "/path/to/file",
+ "-e", "12345=67890",
+ "-f", "12345=67.89",
+ "-g", "DAYS=12.34");
+ assertEquals(app.a.size(), 2);
+ assertEquals(URI.create("/path"), app.a.get(8));
+ assertEquals(URI.create("/path/to/resource"), app.a.get(98765432));
+
+ assertEquals(app.b.size(), 1);
+ assertEquals(new StringBuilder("abc").toString(), app.b.get(TimeUnit.SECONDS).toString());
+
+ assertEquals(app.c.size(), 1);
+ assertEquals("ABC", app.c.get("123"));
+
+ assertEquals(app.d.size(), 1);
+ assertEquals(new File("/path/to/file"), app.d.get(0));
+
+ assertEquals(app.e.size(), 1);
+ assertEquals(new Long(67890), app.e.get(12345));
+
+ assertEquals(app.f.size(), 1);
+ assertEquals(67.89f, app.f.get(new Long(12345)));
+
+ assertEquals(app.g.size(), 1);
+ assertEquals(12.34f, app.g.get(TimeUnit.DAYS));
+ }
+ @Test
+ public void testUseTypeAttributeInsteadOfFieldType() {
+ class App {
+ @Option(names = "--num", type = BigDecimal.class) // subclass of field type
+ Number[] number; // array type with abstract component class
+
+ @Parameters(type = StringBuilder.class) // concrete impl class
+ Appendable address; // type declared as interface
+ }
+ App app = CommandLine.populateCommand(new App(), "--num", "123.456", "ABC");
+ assertEquals(1, app.number.length);
+ assertEquals(new BigDecimal("123.456"), app.number[0]);
+
+ assertEquals("ABC", app.address.toString());
+ assertTrue(app.address instanceof StringBuilder);
+ }
+ @Test
+ public void testMultipleMissingOptions() {
+ class App {
+ @Option(names = "-a", required = true) String first;
+ @Option(names = "-b", required = true) String second;
+ @Option(names = "-c", required = true) String third;
+ }
+ try {
+ CommandLine.populateCommand(new App());
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("Missing required options [-a=<first>, -b=<second>, -c=<third>]", ex.getMessage());
+ }
+ }
+ @Test
+ public void test185MissingOptionsShouldUseLabel() {
+ class App {
+ @Parameters(arity = "1", paramLabel = "IN_FILE", description = "The input file")
+ File foo;
+ @Option(names = "-o", paramLabel = "OUT_FILE", description = "The output file", required = true)
+ File bar;
+ }
+ try {
+ CommandLine.populateCommand(new App());
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("Missing required options [-o=OUT_FILE, params[*]=IN_FILE]", ex.getMessage());
+ }
+ }
+ @Test
+ public void test185MissingMapOptionsShouldUseLabel() {
+ class App {
+ @Parameters(arity = "1", type = {Long.class, File.class}, description = "The input file mapping")
+ Map<Long, File> foo;
+ @Option(names = "-o", description = "The output file mapping", required = true)
+ Map<String, String> bar;
+ @Option(names = "-x", paramLabel = "KEY=VAL", description = "Some other mapping", required = true)
+ Map<String, String> xxx;
+ }
+ try {
+ CommandLine.populateCommand(new App());
+ fail("MissingParameterException expected");
+ } catch (MissingParameterException ex) {
+ assertEquals("Missing required options [-o=<String=String>, -x=KEY=VAL, params[*]=<Long=File>]", ex.getMessage());
+ }
+ }
+ @Test
+ public void testAnyExceptionWrappedInParameterException() {
+ class App {
+ @Option(names = "-queue", type = String.class, split = ",")
+ ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(2);
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-queue a,b,c".split(" "));
+ fail("ParameterException expected");
+ } catch (ParameterException ex) {
+ assertEquals("IllegalStateException: Queue full while processing argument at or before arg[1] 'a,b,c' in [-queue, a,b,c]: java.lang.IllegalStateException: Queue full", ex.getMessage());
+ }
+ }
+ @Test
+ public void test149UnmatchedShortOptionsAreMisinterpretedAsOperands() {
+ class App {
+ @Option(names = "-a") String first;
+ @Option(names = "-b") String second;
+ @Option(names = {"-c", "--ccc"}) String third;
+ @Parameters String[] positional;
+ }
+ //System.setProperty("picocli.trace", "DEBUG");
+ try {
+ CommandLine.populateCommand(new App(), "-xx", "-a", "aValue");
+ fail("UnmatchedArgumentException expected for -xx");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [-xx]", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new App(), "-x", "-a", "aValue");
+ fail("UnmatchedArgumentException expected for -x");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [-x]", ex.getMessage());
+ }
+ try {
+ CommandLine.populateCommand(new App(), "--x", "-a", "aValue");
+ fail("UnmatchedArgumentException expected for --x");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [--x]", ex.getMessage());
+ }
+ }
+ @Test
+ public void test149NonOptionArgsShouldBeTreatedAsOperands() {
+ class App {
+ @Option(names = "/a") String first;
+ @Option(names = "/b") String second;
+ @Option(names = {"/c", "--ccc"}) String third;
+ @Parameters String[] positional;
+ }
+ //System.setProperty("picocli.trace", "DEBUG");
+ App app = CommandLine.populateCommand(new App(), "-yy", "-a");
+ assertArrayEquals(new String[] {"-yy", "-a"}, app.positional);
+
+ app = CommandLine.populateCommand(new App(), "-y", "-a");
+ assertArrayEquals(new String[] {"-y", "-a"}, app.positional);
+
+ app = CommandLine.populateCommand(new App(), "--y", "-a");
+ assertArrayEquals(new String[] {"--y", "-a"}, app.positional);
+ }
+ @Test
+ public void test149LongMatchWeighsWhenDeterminingOptionResemblance() {
+ class App {
+ @Option(names = "/a") String first;
+ @Option(names = "/b") String second;
+ @Option(names = {"/c", "--ccc"}) String third;
+ @Parameters String[] positional;
+ }
+ //System.setProperty("picocli.trace", "DEBUG");
+ try {
+ CommandLine.populateCommand(new App(), "--ccd", "-a");
+ fail("UnmatchedArgumentException expected for --x");
+ } catch (UnmatchedArgumentException ex) {
+ assertEquals("Unmatched argument [--ccd]", ex.getMessage());
+ }
+ }
+ @Test
+ public void test149OnlyUnmatchedOptionStoredOthersParsed() throws Exception {
+ class App {
+ @Option(names = "-a") String first;
+ @Option(names = "-b") String second;
+ @Option(names = {"-c", "--ccc"}) String third;
+ @Parameters String[] positional;
+ }
+ //System.setProperty("picocli.trace", "DEBUG");
+ PrintStream originalErr = System.err;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2500);
+ System.setErr(new PrintStream(baos));
+
+ CommandLine cmd = new CommandL
<TRUNCATED>