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.*;
  *
- * &#064;Command(header = "Encrypt FILE(s), or standard input, to standard output or to the output file.")
+ * &#064;Command(header = "Encrypt FILE(s), or standard input, to standard output or to the output file.",
+ *          version = "v1.2.3")
  * public class Encrypt {
  *
  *     &#064;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
  *     &#064;Option(names = { "-v", "--verbose"}, description = "Verbosely list files processed")
  *     private boolean verbose;
  *
- *     &#064;Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit")
+ *     &#064;Option(names = { "-h", "--help", "-?", "-help"}, usageHelp = true, description = "Display this help and exit")
  *     private boolean help;
+ *
+ *     &#064;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&lt;CommandLine&gt; 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&lt;CommandLine&gt; 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&lt;CommandLine&gt; 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&lt;CommandLine&gt; 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&lt;Object&gt; 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&lt;CommandLine&gt; 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 {
      *     &#064;Option(names = { "-v", "--verbose"}, description = "Verbosely list files processed")
      *     private boolean verbose;
      *
-     *     &#064;Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit")
+     *     &#064;Option(names = { "-h", "--help", "-?", "-help"}, usageHelp = true, description = "Display this help and exit")
      *     private boolean help;
      *
-     *     &#064;Option(names = { "-V", "--version"}, help = true, description = "Display version information and exit")
+     *     &#064;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>