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/08/14 16:18:33 UTC

[2/5] logging-log4j2 git commit: LOG4J2-2011 replace JCommander command line parser with picocli to let users run Log4j2 utility applications without requiring an external dependency

http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/c2818bec/log4j-core/src/test/java/org/apache/logging/log4j/core/util/picocli/CommandLineTest.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/picocli/CommandLineTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/picocli/CommandLineTest.java
new file mode 100644
index 0000000..ab4266a
--- /dev/null
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/picocli/CommandLineTest.java
@@ -0,0 +1,2921 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core.util.picocli;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.Charset;
+import java.sql.Time;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+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.TreeSet;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+import static java.util.concurrent.TimeUnit.*;
+import static org.junit.Assert.*;
+import static org.apache.logging.log4j.core.util.picocli.CommandLine.*;
+
+/**
+ * Tests for the CommandLine argument parsing interpreter functionality.
+ */
+// DONE arrays
+// DONE collection fields
+// DONE all built-in types
+// DONE private fields, public fields (TODO methods?)
+// DONE arity 2, 3
+// DONE arity -1, -2, -3
+// TODO arity ignored for single-value types (non-array, non-collection)
+// DONE positional arguments with '--' separator (where positional arguments look like options)
+// DONE positional arguments without '--' separator (based on arity of last option?)
+// DONE positional arguments based on arity of last option
+// TODO ambiguous options: writing --input ARG (as opposed to --input=ARG) is ambiguous,
+// meaning it is not possible to tell whether ARG is option's argument or a positional argument.
+// In usage patterns this will be interpreted as an option with argument only if a description (covered below)
+// for that option is provided.
+// Otherwise it will be interpreted as an option and separate positional argument.
+// TODO ambiguous short options: ambiguity with the -f FILE and -fFILE notation.
+// In the latter case it is not possible to tell whether it is a number of stacked short options,
+// or an option with an argument. These notations will be interpreted as an option with argument only if a
+// description for the option is provided.
+// DONE compact flags
+// DONE compact flags where last option has an argument, separate by space
+// DONE compact flags where last option has an argument attached to the option character
+// DONE long options with argument separate by space
+// DONE long options with argument separated by '=' (no spaces)
+// TODO document that if arity>1 and args="-opt=val1 val2", arity overrules the "=": both values are assigned
+// TODO test superclass bean and child class bean where child class field shadows super class and have same annotation Option name
+// TODO test superclass bean and child class bean where child class field shadows super class and have different annotation Option name
+// 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 {
+    @Test
+    public void testVersion() {
+        assertEquals("0.9.8", CommandLine.VERSION);
+    }
+
+    private static class SupportedTypes {
+        @Option(names = "-boolean")       boolean booleanField;
+        @Option(names = "-Boolean")       Boolean aBooleanField;
+        @Option(names = "-byte")          byte byteField;
+        @Option(names = "-Byte")          Byte aByteField;
+        @Option(names = "-char")          char charField;
+        @Option(names = "-Character")     Character aCharacterField;
+        @Option(names = "-short")         short shortField;
+        @Option(names = "-Short")         Short aShortField;
+        @Option(names = "-int")           int intField;
+        @Option(names = "-Integer")       Integer anIntegerField;
+        @Option(names = "-long")          long longField;
+        @Option(names = "-Long")          Long aLongField;
+        @Option(names = "-float")         float floatField;
+        @Option(names = "-Float")         Float aFloatField;
+        @Option(names = "-double")        double doubleField;
+        @Option(names = "-Double")        Double aDoubleField;
+        @Option(names = "-String")        String aStringField;
+        @Option(names = "-StringBuilder") StringBuilder aStringBuilderField;
+        @Option(names = "-CharSequence")  CharSequence aCharSequenceField;
+        @Option(names = "-File")          File aFileField;
+        @Option(names = "-URL")           URL anURLField;
+        @Option(names = "-URI")           URI anURIField;
+        @Option(names = "-Date")          Date aDateField;
+        @Option(names = "-Time")          Time aTimeField;
+        @Option(names = "-BigDecimal")    BigDecimal aBigDecimalField;
+        @Option(names = "-BigInteger")    BigInteger aBigIntegerField;
+        @Option(names = "-Charset")       Charset aCharsetField;
+        @Option(names = "-InetAddress")   InetAddress anInetAddressField;
+        @Option(names = "-Pattern")       Pattern aPatternField;
+        @Option(names = "-UUID")          UUID anUUIDField;
+    }
+    @Test
+    public void testDefaults() {
+        SupportedTypes bean = CommandLine.populateCommand(new SupportedTypes());
+        assertEquals("boolean", false, bean.booleanField);
+        assertEquals("Boolean", null, bean.aBooleanField);
+        assertEquals("byte", 0, bean.byteField);
+        assertEquals("Byte", null, bean.aByteField);
+        assertEquals("char", 0, bean.charField);
+        assertEquals("Character", null, bean.aCharacterField);
+        assertEquals("short", 0, bean.shortField);
+        assertEquals("Short", null, bean.aShortField);
+        assertEquals("int", 0, bean.intField);
+        assertEquals("Integer", null, bean.anIntegerField);
+        assertEquals("long", 0, bean.longField);
+        assertEquals("Long", null, bean.aLongField);
+        assertEquals("float", 0f, bean.floatField, Float.MIN_VALUE);
+        assertEquals("Float", null, bean.aFloatField);
+        assertEquals("double", 0d, bean.doubleField, Double.MIN_VALUE);
+        assertEquals("Double", null, bean.aDoubleField);
+        assertEquals("String", null, bean.aStringField);
+        assertEquals("StringBuilder", null, bean.aStringBuilderField);
+        assertEquals("CharSequence", null, bean.aCharSequenceField);
+        assertEquals("File", null, bean.aFileField);
+        assertEquals("URL", null, bean.anURLField);
+        assertEquals("URI", null, bean.anURIField);
+        assertEquals("Date", null, bean.aDateField);
+        assertEquals("Time", null, bean.aTimeField);
+        assertEquals("BigDecimal", null, bean.aBigDecimalField);
+        assertEquals("BigInteger", null, bean.aBigIntegerField);
+        assertEquals("Charset", null, bean.aCharsetField);
+        assertEquals("InetAddress", null, bean.anInetAddressField);
+        assertEquals("Pattern", null, bean.aPatternField);
+        assertEquals("UUID", null, bean.anUUIDField);
+    }
+    @Test
+    public void testTypeConversionSucceedsForValidInput()
+            throws MalformedURLException, URISyntaxException, UnknownHostException, ParseException {
+        SupportedTypes bean = CommandLine.populateCommand(new SupportedTypes(),
+                "-boolean", "-Boolean", //
+                "-byte", "12", "-Byte", "23", //
+                "-char", "p", "-Character", "i", //
+                "-short", "34", "-Short", "45", //
+                "-int", "56", "-Integer", "67", //
+                "-long", "78", "-Long", "89", //
+                "-float", "1.23", "-Float", "2.34", //
+                "-double", "3.45", "-Double", "4.56", //
+                "-String", "abc", "-StringBuilder", "bcd", "-CharSequence", "xyz", //
+                "-File", "abc.txt", //
+                "-URL", "http://pico-cli.github.io", //
+                "-URI", "http://pico-cli.github.io/index.html", //
+                "-Date", "2017-01-30", //
+                "-Time", "23:59:59", //
+                "-BigDecimal", "12345678901234567890.123", //
+                "-BigInteger", "123456789012345678901", //
+                "-Charset", "UTF8", //
+                "-InetAddress", InetAddress.getLocalHost().getHostName(), //
+                "-Pattern", "a*b", //
+                "-UUID", "c7d51423-bf9d-45dd-a30d-5b16fafe42e2"
+        );
+        assertEquals("boolean", true, bean.booleanField);
+        assertEquals("Boolean", Boolean.TRUE, bean.aBooleanField);
+        assertEquals("byte", 12, bean.byteField);
+        assertEquals("Byte", Byte.valueOf((byte) 23), bean.aByteField);
+        assertEquals("char", 'p', bean.charField);
+        assertEquals("Character", Character.valueOf('i'), bean.aCharacterField);
+        assertEquals("short", 34, bean.shortField);
+        assertEquals("Short", Short.valueOf((short) 45), bean.aShortField);
+        assertEquals("int", 56, bean.intField);
+        assertEquals("Integer", Integer.valueOf(67), bean.anIntegerField);
+        assertEquals("long", 78L, bean.longField);
+        assertEquals("Long", Long.valueOf(89L), bean.aLongField);
+        assertEquals("float", 1.23f, bean.floatField, Float.MIN_VALUE);
+        assertEquals("Float", Float.valueOf(2.34f), bean.aFloatField);
+        assertEquals("double", 3.45, bean.doubleField, Double.MIN_VALUE);
+        assertEquals("Double", Double.valueOf(4.56), bean.aDoubleField);
+        assertEquals("String", "abc", bean.aStringField);
+        assertEquals("StringBuilder type", StringBuilder.class, bean.aStringBuilderField.getClass());
+        assertEquals("StringBuilder", "bcd", bean.aStringBuilderField.toString());
+        assertEquals("CharSequence", "xyz", bean.aCharSequenceField);
+        assertEquals("File", new File("abc.txt"), bean.aFileField);
+        assertEquals("URL", new URL("http://pico-cli.github.io"), bean.anURLField);
+        assertEquals("URI", new URI("http://pico-cli.github.io/index.html"), bean.anURIField);
+        assertEquals("Date", new SimpleDateFormat("yyyy-MM-dd").parse("2017-01-30"), bean.aDateField);
+        assertEquals("Time", new Time(new SimpleDateFormat("HH:mm:ss").parse("23:59:59").getTime()), bean.aTimeField);
+        assertEquals("BigDecimal", new BigDecimal("12345678901234567890.123"), bean.aBigDecimalField);
+        assertEquals("BigInteger", new BigInteger("123456789012345678901"), bean.aBigIntegerField);
+        assertEquals("Charset", Charset.forName("UTF8"), bean.aCharsetField);
+        assertEquals("InetAddress", InetAddress.getByName(InetAddress.getLocalHost().getHostName()), bean.anInetAddressField);
+        assertEquals("Pattern", Pattern.compile("a*b").pattern(), bean.aPatternField.pattern());
+        assertEquals("UUID", UUID.fromString("c7d51423-bf9d-45dd-a30d-5b16fafe42e2"), bean.anUUIDField);
+    }
+    @Test
+    public void testByteFieldsAreDecimal() {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-byte", "0x1F", "-Byte", "0x0F");
+            fail("Should fail on hex input");
+        } catch (ParameterException expected) {
+            assertEquals("Could not convert '0x1F' to byte for option '-byte'" +
+                    ": java.lang.NumberFormatException: For input string: \"0x1F\"", expected.getMessage());
+        }
+    }
+    @Test
+    public void testCustomByteConverterAcceptsHexadecimalDecimalAndOctal() {
+        SupportedTypes bean = new SupportedTypes();
+        CommandLine commandLine = new CommandLine(bean);
+        ITypeConverter<Byte> converter = new ITypeConverter<Byte>() {
+            public Byte convert(String s) {
+                return Byte.decode(s);
+            }
+        };
+        commandLine.registerConverter(Byte.class, converter);
+        commandLine.registerConverter(Byte.TYPE, converter);
+        commandLine.parse("-byte", "0x1F", "-Byte", "0x0F");
+        assertEquals(0x1F, bean.byteField);
+        assertEquals(Byte.valueOf((byte) 0x0F), bean.aByteField);
+
+        commandLine.parse("-byte", "010", "-Byte", "010");
+        assertEquals(8, bean.byteField);
+        assertEquals(Byte.valueOf((byte) 8), bean.aByteField);
+
+        commandLine.parse("-byte", "34", "-Byte", "34");
+        assertEquals(34, bean.byteField);
+        assertEquals(Byte.valueOf((byte) 34), bean.aByteField);
+    }
+    @Test
+    public void testShortFieldsAreDecimal() {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-short", "0xFF", "-Short", "0x6FFE");
+            fail("Should fail on hex input");
+        } catch (ParameterException expected) {
+            assertEquals("Could not convert '0xFF' to short for option '-short'" +
+                    ": java.lang.NumberFormatException: For input string: \"0xFF\"", expected.getMessage());
+        }
+    }
+    @Test
+    public void testCustomShortConverterAcceptsHexadecimalDecimalAndOctal() {
+        SupportedTypes bean = new SupportedTypes();
+        CommandLine commandLine = new CommandLine(bean);
+        ITypeConverter<Short> shortConverter = new ITypeConverter<Short>() {
+            public Short convert(String s) {
+                return Short.decode(s);
+            }
+        };
+        commandLine.registerConverter(Short.class, shortConverter);
+        commandLine.registerConverter(Short.TYPE, shortConverter);
+        commandLine.parse("-short", "0xFF", "-Short", "0x6FFE");
+        assertEquals(0xFF, bean.shortField);
+        assertEquals(Short.valueOf((short) 0x6FFE), bean.aShortField);
+
+        commandLine.parse("-short", "010", "-Short", "010");
+        assertEquals(8, bean.shortField);
+        assertEquals(Short.valueOf((short) 8), bean.aShortField);
+
+        commandLine.parse("-short", "34", "-Short", "34");
+        assertEquals(34, bean.shortField);
+        assertEquals(Short.valueOf((short) 34), bean.aShortField);
+    }
+    @Test
+    public void testIntFieldsAreDecimal() {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-int", "0xFF", "-Integer", "0xFFFF");
+            fail("Should fail on hex input");
+        } catch (ParameterException expected) {
+            assertEquals("Could not convert '0xFF' to int for option '-int'" +
+                    ": java.lang.NumberFormatException: For input string: \"0xFF\"", expected.getMessage());
+        }
+    }
+    @Test
+    public void testCustomIntConverterAcceptsHexadecimalDecimalAndOctal() {
+        SupportedTypes bean = new SupportedTypes();
+        CommandLine commandLine = new CommandLine(bean);
+        ITypeConverter<Integer> intConverter = new ITypeConverter<Integer>() {
+            public Integer convert(String s) {
+                return Integer.decode(s);
+            }
+        };
+        commandLine.registerConverter(Integer.class, intConverter);
+        commandLine.registerConverter(Integer.TYPE, intConverter);
+        commandLine.parse("-int", "0xFF", "-Integer", "0xFFFF");
+        assertEquals(255, bean.intField);
+        assertEquals(Integer.valueOf(0xFFFF), bean.anIntegerField);
+
+        commandLine.parse("-int", "010", "-Integer", "010");
+        assertEquals(8, bean.intField);
+        assertEquals(Integer.valueOf(8), bean.anIntegerField);
+
+        commandLine.parse("-int", "34", "-Integer", "34");
+        assertEquals(34, bean.intField);
+        assertEquals(Integer.valueOf(34), bean.anIntegerField);
+    }
+    @Test
+    public void testLongFieldsAreDecimal() {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-long", "0xAABBCC", "-Long", "0xAABBCCDD");
+            fail("Should fail on hex input");
+        } catch (ParameterException expected) {
+            assertEquals("Could not convert '0xAABBCC' to long for option '-long'" +
+                    ": java.lang.NumberFormatException: For input string: \"0xAABBCC\"", expected.getMessage());
+        }
+    }
+    @Test
+    public void testCustomLongConverterAcceptsHexadecimalDecimalAndOctal() {
+        SupportedTypes bean = new SupportedTypes();
+        CommandLine commandLine = new CommandLine(bean);
+        ITypeConverter<Long> longConverter = new ITypeConverter<Long>() {
+            public Long convert(String s) {
+                return Long.decode(s);
+            }
+        };
+        commandLine.registerConverter(Long.class, longConverter);
+        commandLine.registerConverter(Long.TYPE, longConverter);
+        commandLine.parse("-long", "0xAABBCC", "-Long", "0xAABBCCDD");
+        assertEquals(0xAABBCC, bean.longField);
+        assertEquals(Long.valueOf(0xAABBCCDDL), bean.aLongField);
+
+        commandLine.parse("-long", "010", "-Long", "010");
+        assertEquals(8, bean.longField);
+        assertEquals(Long.valueOf(8), bean.aLongField);
+
+        commandLine.parse("-long", "34", "-Long", "34");
+        assertEquals(34, bean.longField);
+        assertEquals(Long.valueOf(34), bean.aLongField);
+    }
+    @Test(expected = MissingParameterException.class)
+    public void testSingleValueFieldDefaultMinArityIs1() {
+        CommandLine.populateCommand(new SupportedTypes(),  "-Long");
+    }
+    @Test
+    public void testSingleValueFieldDefaultMinArityIsOne() {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(),  "-Long", "-boolean");
+            fail("should fail");
+        } catch (ParameterException ex) {
+            assertEquals("Could not convert '-boolean' to Long for option '-Long'" +
+                    ": java.lang.NumberFormatException: For input string: \"-boolean\"", ex.getMessage());
+        }
+    }
+    @Test
+    public void testTimeFormatHHmmSupported() throws ParseException {
+        SupportedTypes bean = CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59");
+        assertEquals("Time", new Time(new SimpleDateFormat("HH:mm").parse("23:59").getTime()), bean.aTimeField);
+    }
+    @Test
+    public void testTimeFormatHHmmssSupported() throws ParseException {
+        SupportedTypes bean = CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:58");
+        assertEquals("Time", new Time(new SimpleDateFormat("HH:mm:ss").parse("23:59:58").getTime()), bean.aTimeField);
+    }
+    @Test
+    public void testTimeFormatHHmmssDotSSSSupported() throws ParseException {
+        SupportedTypes bean = CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:58.123");
+        assertEquals("Time", new Time(new SimpleDateFormat("HH:mm:ss.SSS").parse("23:59:58.123").getTime()), bean.aTimeField);
+    }
+    @Test
+    public void testTimeFormatHHmmssCommaSSSSupported() throws ParseException {
+        SupportedTypes bean = CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:58,123");
+        assertEquals("Time", new Time(new SimpleDateFormat("HH:mm:ss,SSS").parse("23:59:58,123").getTime()), bean.aTimeField);
+    }
+    @Test
+    public void testTimeFormatHHmmssSSSInvalidError() throws ParseException {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:58;123");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'23:59:58;123' is not a HH:mm[:ss[.SSS]] time for option '-Time'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testTimeFormatHHmmssDotInvalidError() throws ParseException {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:58.");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'23:59:58.' is not a HH:mm[:ss[.SSS]] time for option '-Time'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testTimeFormatHHmmsssInvalidError() throws ParseException {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:587");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'23:59:587' is not a HH:mm[:ss[.SSS]] time for option '-Time'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testTimeFormatHHmmssColonInvalidError() throws ParseException {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-Time", "23:59:");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'23:59:' is not a HH:mm[:ss[.SSS]] time for option '-Time'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testDateFormatYYYYmmddInvalidError() throws ParseException {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-Date", "20170131");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'20170131' is not a yyyy-MM-dd date for option '-Date'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testCharConverterInvalidError() throws ParseException {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-Character", "aa");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'aa' is not a single character for option '-Character'", expected.getMessage());
+        }
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), "-char", "aa");
+            fail("Invalid format was accepted");
+        } catch (ParameterException expected) {
+            assertEquals("'aa' is not a single character for option '-char'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testNumberConvertersInvalidError() {
+        parseInvalidValue("-Byte", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-byte", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-Short", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-short", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-Integer", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-int", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-Long", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-long", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-Float", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-float", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-Double", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-double", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+        parseInvalidValue("-BigDecimal", "aa", ": java.lang.NumberFormatException");
+        parseInvalidValue("-BigInteger", "aa", ": java.lang.NumberFormatException: For input string: \"aa\"");
+    }
+    @Test
+    public void testURLConvertersInvalidError() {
+        parseInvalidValue("-URL", ":::", ": java.net.MalformedURLException: no protocol: :::");
+    }
+    @Test
+    public void testURIConvertersInvalidError() {
+        parseInvalidValue("-URI", ":::", ": java.net.URISyntaxException: Expected scheme name at index 0: :::");
+    }
+    @Test
+    public void testCharsetConvertersInvalidError() {
+        parseInvalidValue("-Charset", "aa", ": java.nio.charset.UnsupportedCharsetException: aa");
+    }
+    @Test
+    public void testInetAddressConvertersInvalidError() {
+        parseInvalidValue("-InetAddress", "%$::a?*!a", ": java.net.UnknownHostException: %$::a?*!a");
+    }
+    @Test
+    public void testUUIDConvertersInvalidError() {
+        parseInvalidValue("-UUID", "aa", ": java.lang.IllegalArgumentException: Invalid UUID string: aa");
+    }
+    @Test
+    public void testRegexPatternConverterInvalidError() {
+        parseInvalidValue("-Pattern", "[[(aa", String.format(": java.util.regex.PatternSyntaxException: Unclosed character class near index 4%n" +
+                "[[(aa%n" +
+                "    ^"));
+    }
+
+    private void parseInvalidValue(String option, String value, String errorMessage) {
+        try {
+            CommandLine.populateCommand(new SupportedTypes(), option, value);
+            fail("Invalid format " + value + " was accepted for " + option);
+        } catch (ParameterException actual) {
+            String type = option.substring(1);
+            String expected = "Could not convert '" + value + "' to " + type + " for option '" + option + "'" + errorMessage;
+            assertTrue("expected:<" + expected + "> but was:<" + actual.getMessage() + ">",
+                    actual.getMessage().startsWith(actual.getMessage()));
+        }
+    }
+
+    @Test
+    public void testCustomConverter() {
+        class Glob {
+            public final String glob;
+            public Glob(String glob) { this.glob = glob; }
+        }
+        class App {
+            @Parameters Glob globField;
+        }
+        class GlobConverter implements ITypeConverter<Glob> {
+            public Glob convert(String value) throws Exception { return new Glob(value); }
+        }
+        CommandLine commandLine = new CommandLine(new App());
+        commandLine.registerConverter(Glob.class, new GlobConverter());
+
+        String[] args = {"a*glob*pattern"};
+        List<CommandLine> parsed = commandLine.parse(args);
+        assertEquals("not empty", 1, parsed.size());
+        assertTrue(parsed.get(0).getCommand() instanceof App);
+        App app = (App) parsed.get(0).getCommand();
+        assertEquals(args[0], app.globField.glob);
+    }
+
+    static class EnumParams {
+        @Option(names = "-timeUnit") TimeUnit timeUnit;
+        @Option(names = "-timeUnitArray", arity = "2") TimeUnit[] timeUnitArray;
+        @Option(names = "-timeUnitList", type = TimeUnit.class, arity = "3") List<TimeUnit> timeUnitList;
+    }
+    @Test
+    public void testEnumTypeConversionSuceedsForValidInput() {
+        EnumParams params = CommandLine.populateCommand(new EnumParams(),
+                "-timeUnit DAYS -timeUnitArray HOURS MINUTES -timeUnitList SECONDS MICROSECONDS NANOSECONDS".split(" "));
+        assertEquals(DAYS, params.timeUnit);
+        assertArrayEquals(new TimeUnit[]{HOURS, TimeUnit.MINUTES}, params.timeUnitArray);
+        List<TimeUnit> expected = new ArrayList<TimeUnit>(Arrays.asList(TimeUnit.SECONDS, TimeUnit.MICROSECONDS, TimeUnit.NANOSECONDS));
+        assertEquals(expected, params.timeUnitList);
+    }
+    @Test
+    public void testEnumTypeConversionFailsForInvalidInput() {
+        try {
+            CommandLine.populateCommand(new EnumParams(), "-timeUnit", "xyz");
+            fail("Accepted invalid timeunit");
+        } catch (Exception ex) {
+            assertEquals("Could not convert 'xyz' to TimeUnit for option '-timeUnit'" +
+                    ": java.lang.IllegalArgumentException: No enum constant java.util.concurrent.TimeUnit.xyz", ex.getMessage());
+        }
+    }
+    @Ignore("Requires #14 case-insensitive enum parsing")
+    @Test
+    public void testEnumTypeConversionIsCaseInsensitive() {
+        EnumParams params = CommandLine.populateCommand(new EnumParams(),
+                "-timeUnit daYS -timeUnitArray hours miNutEs -timeUnitList SEConds MiCROsEconds nanoSEConds".split(" "));
+        assertEquals(DAYS, params.timeUnit);
+        assertArrayEquals(new TimeUnit[]{HOURS, TimeUnit.MINUTES}, params.timeUnitArray);
+        List<TimeUnit> expected = new ArrayList<TimeUnit>(Arrays.asList(TimeUnit.SECONDS, TimeUnit.MICROSECONDS, TimeUnit.NANOSECONDS));
+        assertEquals(expected, params.timeUnitList);
+    }
+    @Test
+    public void testEnumArrayTypeConversionFailsForInvalidInput() {
+        try {
+            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)" +
+                    ": java.lang.IllegalArgumentException: No enum constant java.util.concurrent.TimeUnit.a", ex.getMessage());
+        }
+    }
+    @Test
+    public void testEnumListTypeConversionFailsForInvalidInput() {
+        try {
+            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)" +
+                    ": java.lang.IllegalArgumentException: No enum constant java.util.concurrent.TimeUnit.b",
+                    ex.getMessage());
+        }
+    }
+
+    @Test
+    public void testArrayOptionParametersAreAlwaysInstantiated() {
+        EnumParams params = new EnumParams();
+        TimeUnit[] array = params.timeUnitArray;
+        new CommandLine(params).parse("-timeUnitArray", "DAYS", "HOURS");
+        assertNotSame(array, params.timeUnitArray);
+    }
+    @Test
+    public void testListOptionParametersAreInstantiatedIfNull() {
+        EnumParams params = new EnumParams();
+        assertNull(params.timeUnitList);
+        new CommandLine(params).parse("-timeUnitList", "DAYS", "HOURS", "DAYS");
+        assertEquals(Arrays.asList(DAYS, HOURS, DAYS), params.timeUnitList);
+    }
+    @Test
+    public void testListOptionParametersAreReusedInstantiatedIfNonNull() {
+        EnumParams params = new EnumParams();
+        List<TimeUnit> list = new ArrayList<TimeUnit>();
+        params.timeUnitList = list;
+        new CommandLine(params).parse("-timeUnitList", "DAYS", "HOURS", "DAYS");
+        assertEquals(Arrays.asList(DAYS, HOURS, DAYS), params.timeUnitList);
+        assertSame(list, params.timeUnitList);
+    }
+    @Test
+    public void testArrayPositionalParametersAreAppendedNotReplaced() {
+        class ArrayPositionalParams {
+            @Parameters() int[] array;
+        }
+        ArrayPositionalParams params = new ArrayPositionalParams();
+        params.array = new int[3];
+        int[] array = params.array;
+        new CommandLine(params).parse("3", "2", "1");
+        assertNotSame(array, params.array);
+        assertArrayEquals(new int[]{0, 0, 0, 3, 2, 1}, params.array);
+    }
+    class ListPositionalParams {
+        @Parameters(type = Integer.class) List<Integer> list;
+    }
+    @Test
+    public void testListPositionalParametersAreInstantiatedIfNull() {
+        ListPositionalParams params = new ListPositionalParams();
+        assertNull(params.list);
+        new CommandLine(params).parse("3", "2", "1");
+        assertNotNull(params.list);
+        assertEquals(Arrays.asList(3, 2, 1), params.list);
+    }
+    @Test
+    public void testListPositionalParametersAreReusedIfNonNull() {
+        ListPositionalParams params = new ListPositionalParams();
+        params.list = new ArrayList<Integer>();
+        List<Integer> list = params.list;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.list);
+        assertEquals(Arrays.asList(3, 2, 1), params.list);
+    }
+    @Test
+    public void testListPositionalParametersAreAppendedToIfNonNull() {
+        ListPositionalParams params = new ListPositionalParams();
+        params.list = new ArrayList<Integer>();
+        params.list.add(234);
+        List<Integer> list = params.list;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.list);
+        assertEquals(Arrays.asList(234, 3, 2, 1), params.list);
+    }
+    class SortedSetPositionalParams {
+        @Parameters(type = Integer.class) SortedSet<Integer> sortedSet;
+    }
+    @Test
+    public void testSortedSetPositionalParametersAreInstantiatedIfNull() {
+        SortedSetPositionalParams params = new SortedSetPositionalParams();
+        assertNull(params.sortedSet);
+        new CommandLine(params).parse("3", "2", "1");
+        assertNotNull(params.sortedSet);
+        assertEquals(Arrays.asList(1, 2, 3), new ArrayList<Integer>(params.sortedSet));
+    }
+    @Test
+    public void testSortedSetPositionalParametersAreReusedIfNonNull() {
+        SortedSetPositionalParams params = new SortedSetPositionalParams();
+        params.sortedSet = new TreeSet<Integer>();
+        SortedSet<Integer> list = params.sortedSet;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.sortedSet);
+        assertEquals(Arrays.asList(1, 2, 3), new ArrayList<Integer>(params.sortedSet));
+    }
+    @Test
+    public void testSortedSetPositionalParametersAreAppendedToIfNonNull() {
+        SortedSetPositionalParams params = new SortedSetPositionalParams();
+        params.sortedSet = new TreeSet<Integer>();
+        params.sortedSet.add(234);
+        SortedSet<Integer> list = params.sortedSet;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.sortedSet);
+        assertEquals(Arrays.asList(1, 2, 3, 234), new ArrayList<Integer>(params.sortedSet));
+    }
+    class SetPositionalParams {
+        @Parameters(type = Integer.class) Set<Integer> set;
+    }
+    @Test
+    public void testSetPositionalParametersAreInstantiatedIfNull() {
+        SetPositionalParams params = new SetPositionalParams();
+        assertNull(params.set);
+        new CommandLine(params).parse("3", "2", "1");
+        assertNotNull(params.set);
+        assertEquals(new HashSet(Arrays.asList(1, 2, 3)), params.set);
+    }
+    @Test
+    public void testSetPositionalParametersAreReusedIfNonNull() {
+        SetPositionalParams params = new SetPositionalParams();
+        params.set = new TreeSet<Integer>();
+        Set<Integer> list = params.set;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.set);
+        assertEquals(new HashSet(Arrays.asList(1, 2, 3)), params.set);
+    }
+    @Test
+    public void testSetPositionalParametersAreAppendedToIfNonNull() {
+        SetPositionalParams params = new SetPositionalParams();
+        params.set = new TreeSet<Integer>();
+        params.set.add(234);
+        Set<Integer> list = params.set;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.set);
+        assertEquals(new HashSet(Arrays.asList(1, 2, 3, 234)), params.set);
+    }
+    class QueuePositionalParams {
+        @Parameters(type = Integer.class) Queue<Integer> queue;
+    }
+    @Test
+    public void testQueuePositionalParametersAreInstantiatedIfNull() {
+        QueuePositionalParams params = new QueuePositionalParams();
+        assertNull(params.queue);
+        new CommandLine(params).parse("3", "2", "1");
+        assertNotNull(params.queue);
+        assertEquals(new LinkedList(Arrays.asList(3, 2, 1)), params.queue);
+    }
+    @Test
+    public void testQueuePositionalParametersAreReusedIfNonNull() {
+        QueuePositionalParams params = new QueuePositionalParams();
+        params.queue = new LinkedList<Integer>();
+        Queue<Integer> list = params.queue;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.queue);
+        assertEquals(new LinkedList(Arrays.asList(3, 2, 1)), params.queue);
+    }
+    @Test
+    public void testQueuePositionalParametersAreAppendedToIfNonNull() {
+        QueuePositionalParams params = new QueuePositionalParams();
+        params.queue = new LinkedList<Integer>();
+        params.queue.add(234);
+        Queue<Integer> list = params.queue;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.queue);
+        assertEquals(new LinkedList(Arrays.asList(234, 3, 2, 1)), params.queue);
+    }
+    class CollectionPositionalParams {
+        @Parameters(type = Integer.class) Collection<Integer> collection;
+    }
+    @Test
+    public void testCollectionPositionalParametersAreInstantiatedIfNull() {
+        CollectionPositionalParams params = new CollectionPositionalParams();
+        assertNull(params.collection);
+        new CommandLine(params).parse("3", "2", "1");
+        assertNotNull(params.collection);
+        assertEquals(Arrays.asList(3, 2, 1), params.collection);
+    }
+    @Test
+    public void testCollectionPositionalParametersAreReusedIfNonNull() {
+        CollectionPositionalParams params = new CollectionPositionalParams();
+        params.collection = new ArrayList<Integer>();
+        Collection<Integer> list = params.collection;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.collection);
+        assertEquals(Arrays.asList(3, 2, 1), params.collection);
+    }
+    @Test
+    public void testCollectionPositionalParametersAreAppendedToIfNonNull() {
+        CollectionPositionalParams params = new CollectionPositionalParams();
+        params.collection = new ArrayList<Integer>();
+        params.collection.add(234);
+        Collection<Integer> list = params.collection;
+        new CommandLine(params).parse("3", "2", "1");
+        assertSame(list, params.collection);
+        assertEquals(Arrays.asList(234, 3, 2, 1), params.collection);
+    }
+
+    @Test(expected = DuplicateOptionAnnotationsException.class)
+    public void testDuplicateOptionsAreRejected() {
+        /** Duplicate parameter names are invalid. */
+        class DuplicateOptions {
+            @Option(names = "-duplicate") public int value1;
+            @Option(names = "-duplicate") public int value2;
+        }
+        new CommandLine(new DuplicateOptions());
+    }
+
+    @Test(expected = ParameterException.class)
+    public void testClashingAnnotationsAreRejected() {
+        class ClashingAnnotation {
+            @Option(names = "-o")
+            @Parameters
+            public String[] bothOptionAndParameters;
+        }
+        new CommandLine(new ClashingAnnotation());
+    }
+
+    private static class PrivateFinalOptionFields {
+        @Option(names = "-f") private final String field = null;
+        @Option(names = "-p") private final int primitive = 43;
+    }
+    @Test
+    public void testCanInitializePrivateFinalFields() {
+        PrivateFinalOptionFields ff = CommandLine.populateCommand(new PrivateFinalOptionFields(), "-f", "reference value");
+        assertEquals("reference value", ff.field);
+    }
+    @Ignore("Needs Reject final primitive fields annotated with @Option or @Parameters #68")
+    @Test
+    public void testCanInitializeFinalPrimitiveFields() {
+        PrivateFinalOptionFields ff = CommandLine.populateCommand(new PrivateFinalOptionFields(), "-p", "12");
+        assertEquals("primitive value", 12, ff.primitive);
+    }
+    @Test
+    public void testLastValueSelectedIfOptionSpecifiedMultipleTimes() {
+        CommandLine cmd = new CommandLine(new PrivateFinalOptionFields()).setOverwrittenOptionsAllowed(true);
+        cmd.parse("-f", "111", "-f", "222");
+        PrivateFinalOptionFields ff = (PrivateFinalOptionFields) cmd.getCommand();
+        assertEquals("222", ff.field);
+    }
+
+    private static class PrivateFinalParameterFields {
+        @Parameters(index = "0") private final String field = null;
+        @Parameters(index = "1", arity = "0..1") private final int primitive = 43;
+    }
+    @Test
+    public void testCanInitializePrivateFinalParameterFields() {
+        PrivateFinalParameterFields ff = CommandLine.populateCommand(new PrivateFinalParameterFields(), "ref value");
+        assertEquals("ref value", ff.field);
+    }
+    @Ignore("Needs Reject final primitive fields annotated with @Option or @Parameters #68")
+    @Test
+    public void testCannotInitializePrivateFinalPrimitiveParameterFields() {
+        PrivateFinalParameterFields ff = CommandLine.populateCommand(new PrivateFinalParameterFields(), "ref value", "12");
+        assertEquals("ref value", ff.field);
+        assertEquals("primitive value", 12, ff.primitive);
+    }
+
+    private static class RequiredField {
+        @Option(names = {"-?", "/?"},        help = true)       boolean isHelpRequested;
+        @Option(names = {"-V", "--version"}, versionHelp= true) boolean versionHelp;
+        @Option(names = {"-h", "--help"},    usageHelp = true)  boolean usageHelp;
+        @Option(names = "--required", required = true) private String required;
+        @Parameters private String[] remainder;
+    }
+    @Test
+    public void testErrorIfRequiredOptionNotSpecified() {
+        try {
+            CommandLine.populateCommand(new RequiredField(), "arg1", "arg2");
+            fail("Missing required field should have thrown exception");
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required option 'required'", ex.getMessage());
+        }
+    }
+    @Test
+    public void testNoErrorIfRequiredOptionSpecified() {
+        CommandLine.populateCommand(new RequiredField(), "--required", "arg1", "arg2");
+    }
+    @Test
+    public void testNoErrorIfRequiredOptionNotSpecifiedWhenHelpRequested() {
+        RequiredField requiredField = CommandLine.populateCommand(new RequiredField(), "-?");
+        assertTrue("help requested", requiredField.isHelpRequested);
+    }
+    @Test
+    public void testNoErrorIfRequiredOptionNotSpecifiedWhenUsageHelpRequested() {
+        RequiredField requiredField = CommandLine.populateCommand(new RequiredField(), "--help");
+        assertTrue("usage help requested", requiredField.usageHelp);
+    }
+    @Test
+    public void testNoErrorIfRequiredOptionNotSpecifiedWhenVersionHelpRequested() {
+        RequiredField requiredField = CommandLine.populateCommand(new RequiredField(), "--version");
+        assertTrue("version info requested", requiredField.versionHelp);
+    }
+    @Test
+    public void testCommandLine_isUsageHelpRequested_trueWhenSpecified() {
+        List<CommandLine> parsedCommands = new CommandLine(new RequiredField()).parse("--help");
+        assertTrue("usage help requested", parsedCommands.get(0).isUsageHelpRequested());
+    }
+    @Test
+    public void testCommandLine_isVersionHelpRequested_trueWhenSpecified() {
+        List<CommandLine> parsedCommands = new CommandLine(new RequiredField()).parse("--version");
+        assertTrue("version info requested", parsedCommands.get(0).isVersionHelpRequested());
+    }
+    @Test
+    public void testCommandLine_isUsageHelpRequested_falseWhenNotSpecified() {
+        List<CommandLine> parsedCommands = new CommandLine(new RequiredField()).parse("--version");
+        assertFalse("usage help requested", parsedCommands.get(0).isUsageHelpRequested());
+    }
+    @Test
+    public void testCommandLine_isVersionHelpRequested_falseWhenNotSpecified() {
+        List<CommandLine> parsedCommands = new CommandLine(new RequiredField()).parse("--help");
+        assertFalse("version info requested", parsedCommands.get(0).isVersionHelpRequested());
+    }
+    @Test
+    public void testMissingRequiredParams() {
+        class Example {
+            @Parameters(index = "1", arity = "0..1") String optional;
+            @Parameters(index = "0") String mandatory;
+        }
+        try { CommandLine.populateCommand(new Example(), new String[] {"mandatory"}); }
+        catch (MissingParameterException ex) { fail(); }
+
+        try {
+            CommandLine.populateCommand(new Example(), new String[0]);
+            fail("Should not accept missing mandatory parameter");
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required parameter: mandatory", ex.getMessage());
+        }
+    }
+    @Test
+    public void testMissingRequiredParams1() {
+        class Tricky1 {
+            @Parameters(index = "2") String anotherMandatory;
+            @Parameters(index = "1", arity = "0..1") String optional;
+            @Parameters(index = "0") String mandatory;
+        }
+        try {
+            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());
+        }
+        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());
+        }
+    }
+    @Test
+    public void testMissingRequiredParams2() {
+        class Tricky2 {
+            @Parameters(index = "2", arity = "0..1") String anotherOptional;
+            @Parameters(index = "1", arity = "0..1") String optional;
+            @Parameters(index = "0") String mandatory;
+        }
+        try { CommandLine.populateCommand(new Tricky2(), new String[] {"mandatory"}); }
+        catch (MissingParameterException ex) { fail(); }
+
+        try {
+            CommandLine.populateCommand(new Tricky2(), new String[0]);
+            fail("Should not accept missing mandatory parameter");
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required parameter: mandatory", ex.getMessage());
+        }
+    }
+    @Test
+    public void testMissingRequiredParamsWithOptions() {
+        class Tricky3 {
+            @Option(names="-v") boolean more;
+            @Option(names="-t") boolean any;
+            @Parameters(index = "1") String alsoMandatory;
+            @Parameters(index = "0") String mandatory;
+        }
+        try {
+            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());
+        }
+
+        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());
+        }
+    }
+    @Test
+    public void testMissingRequiredParamWithOption() {
+        class Tricky3 {
+            @Option(names="-t") boolean any;
+            @Parameters(index = "0") String mandatory;
+        }
+        try {
+            CommandLine.populateCommand(new Tricky3(), new String[] {"-t"});
+            fail("Should not accept missing mandatory parameter");
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required parameter: mandatory", ex.getMessage());
+        }
+    }
+    @Test
+    public void testNoMissingRequiredParamErrorIfHelpOptionSpecified() {
+        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")    InetAddress  host;
+            @Parameters(index = "1")    int          port;
+            @Parameters(index = "2..*") 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
+    public void testHelpRequestedFlagResetWhenParsing_staticMethod() {
+        RequiredField requiredField = CommandLine.populateCommand(new RequiredField(), "-?");
+        assertTrue("help requested", requiredField.isHelpRequested);
+
+        requiredField.isHelpRequested = false;
+
+        // should throw error again on second pass (no help was requested here...)
+        try {
+            CommandLine.populateCommand(requiredField, "arg1", "arg2");
+            fail("Missing required field should have thrown exception");
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required option 'required'", ex.getMessage());
+        }
+    }
+    @Test
+    public void testHelpRequestedFlagResetWhenParsing_instanceMethod() {
+        RequiredField requiredField = new RequiredField();
+        CommandLine commandLine = new CommandLine(requiredField);
+        commandLine.parse("-?");
+        assertTrue("help requested", requiredField.isHelpRequested);
+
+        requiredField.isHelpRequested = false;
+
+        // should throw error again on second pass (no help was requested here...)
+        try {
+            commandLine.parse("arg1", "arg2");
+            fail("Missing required field should have thrown exception");
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required option 'required'", ex.getMessage());
+        }
+    }
+
+    private class CompactFields {
+        @Option(names = "-v") boolean verbose;
+        @Option(names = "-r") boolean recursive;
+        @Option(names = "-o") File outputFile;
+        @Parameters File[] inputFiles;
+    }
+    @Test
+    public void testCompactFieldsAnyOrder() {
+        //cmd -a -o arg path path
+        //cmd -o arg -a path path
+        //cmd -a -o arg -- path path
+        //cmd -a -oarg path path
+        //cmd -aoarg path path
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-rvoout");
+        verifyCompact(compact, true, true, "out", null);
+
+        // change order within compact group
+        compact = CommandLine.populateCommand(new CompactFields(), "-vroout");
+        verifyCompact(compact, true, true, "out", null);
+
+        compact = CommandLine.populateCommand(new CompactFields(), "-rv p1 p2".split(" "));
+        verifyCompact(compact, true, true, null, fileArray("p1", "p2"));
+
+        compact = CommandLine.populateCommand(new CompactFields(), "-voout p1 p2".split(" "));
+        verifyCompact(compact, true, false, "out", fileArray("p1", "p2"));
+
+        compact = CommandLine.populateCommand(new CompactFields(), "-voout -r p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+
+        compact = CommandLine.populateCommand(new CompactFields(), "-r -v -oout p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+
+        compact = CommandLine.populateCommand(new CompactFields(), "-oout -r -v p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+
+        compact = CommandLine.populateCommand(new CompactFields(), "-rvo out p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+
+        try {
+            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());
+        }
+    }
+
+    @Test
+    public void testCompactFieldsWithUnmatchedArguments() {
+        CommandLine cmd = new CommandLine(new CompactFields()).setUnmatchedArgumentsAllowed(true);
+        cmd.parse("-oout -r -vp1 p2".split(" "));
+        assertEquals(Arrays.asList("-p1", "p2"), cmd.getUnmatchedArguments());
+    }
+
+    @Test
+    public void testCompactWithOptionParamSeparatePlusParameters() {
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-r -v -o out p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+    }
+
+    @Test
+    public void testCompactWithOptionParamAttachedEqualsSeparatorChar() {
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-rvo=out p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+    }
+
+    @Test
+    public void testCompactWithOptionParamAttachedColonSeparatorChar() {
+        CompactFields compact = new CompactFields();
+        CommandLine cmd = new CommandLine(compact);
+        cmd.setSeparator(":");
+        cmd.parse("-rvo:out p1 p2".split(" "));
+        verifyCompact(compact, true, true, "out", fileArray("p1", "p2"));
+    }
+
+    /** See {@link #testGnuLongOptionsWithVariousSeparators()}  */
+    @Test
+    public void testDefaultSeparatorIsEquals() {
+        assertEquals("=", new CommandLine(new CompactFields()).getSeparator());
+    }
+
+    @Test
+    public void testOptionsAfterParamAreInterpretedAsParameters() {
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-r -v p1 -o out p2".split(" "));
+        verifyCompact(compact, true, true, null, fileArray("p1", "-o", "out", "p2"));
+    }
+    @Test
+    public void testShortOptionsWithSeparatorButNoValueAssignsEmptyStringEvenIfNotLast() {
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-ro= -v".split(" "));
+        verifyCompact(compact, true, true, "", null);
+    }
+    @Test
+    public void testShortOptionsWithColonSeparatorButNoValueAssignsEmptyStringEvenIfNotLast() {
+        CompactFields compact = new CompactFields();
+        CommandLine cmd = new CommandLine(compact);
+        cmd.setSeparator(":");
+        cmd.parse("-ro: -v".split(" "));
+        verifyCompact(compact, true, true, "", null);
+    }
+    @Test
+    public void testShortOptionsWithSeparatorButNoValueAssignsEmptyStringIfLast() {
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-rvo=".split(" "));
+        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"));
+    }
+
+    private File[] fileArray(final String ... paths) {
+        File[] result = new File[paths.length];
+        for (int i = 0; i < result.length; i++) {
+            result[i] = new File(paths[i]);
+        }
+        return result;
+    }
+
+    private void verifyCompact(CompactFields compact, boolean verbose, boolean recursive, String out, File[] params) {
+        assertEquals("-v", verbose, compact.verbose);
+        assertEquals("-r", recursive, compact.recursive);
+        assertEquals("-o", out == null ? null : new File(out), compact.outputFile);
+        if (params == null) {
+            assertNull("args", compact.inputFiles);
+        } else {
+            assertArrayEquals("args=" + Arrays.toString(compact.inputFiles), params, compact.inputFiles);
+        }
+    }
+
+    @Test
+    public void testNonSpacedOptions() {
+        CompactFields compact = CommandLine.populateCommand(new CompactFields(), "-rvo arg path path".split(" "));
+        assertTrue("-r", compact.recursive);
+        assertTrue("-v", compact.verbose);
+        assertEquals("-o", new File("arg"), compact.outputFile);
+        assertArrayEquals("args", new File[]{new File("path"), new File("path")}, compact.inputFiles);
+    }
+
+    @Test
+    public void testPrimitiveParameters() {
+        class PrimitiveIntParameters {
+            @Parameters int[] intParams;
+        }
+        PrimitiveIntParameters params = CommandLine.populateCommand(new PrimitiveIntParameters(), "1 2 3 4".split(" "));
+        assertArrayEquals(new int[] {1, 2, 3, 4}, params.intParams);
+    }
+
+    @Test
+    public void testArityConstructor_fixedRange() {
+        Range arity = new Range(1, 23, false, false, null);
+        assertEquals("min", 1, arity.min);
+        assertEquals("max", 23, arity.max);
+        assertEquals("1..23", arity.toString());
+        assertEquals(Range.valueOf("1..23"), arity);
+    }
+    @Test
+    public void testArityConstructor_variableRange() {
+        Range arity = new Range(1, Integer.MAX_VALUE, true, false, null);
+        assertEquals("min", 1, arity.min);
+        assertEquals("max", Integer.MAX_VALUE, arity.max);
+        assertEquals("1..*", arity.toString());
+        assertEquals(Range.valueOf("1..*"), arity);
+    }
+    @Test
+    public void testArityForOption_booleanFieldImplicitArity0() throws Exception {
+        Range arity = Range.optionArity(SupportedTypes.class.getDeclaredField("booleanField"));
+        assertEquals(Range.valueOf("0"), arity);
+        assertEquals("0", arity.toString());
+    }
+    @Test
+    public void testArityForOption_intFieldImplicitArity1() throws Exception {
+        Range arity = Range.optionArity(SupportedTypes.class.getDeclaredField("intField"));
+        assertEquals(Range.valueOf("1"), arity);
+        assertEquals("1", arity.toString());
+    }
+    @Test
+    public void testArityForOption_isExplicitlyDeclaredValue() throws Exception {
+        Range arity = Range.optionArity(EnumParams.class.getDeclaredField("timeUnitList"));
+        assertEquals(Range.valueOf("3"), arity);
+        assertEquals("3", arity.toString());
+    }
+    @Test
+    public void testArityForOption_listFieldImplicitArity0_n() 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());
+    }
+    @Test
+    public void testArityForOption_arrayFieldImplicitArity0_n() 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());
+    }
+    @Test
+    public void testArityForParameters_booleanFieldImplicitArity0() throws Exception {
+        class ImplicitBoolField { @Parameters boolean boolSingleValue; }
+        Range arity = Range.parameterArity(ImplicitBoolField.class.getDeclaredField("boolSingleValue"));
+        assertEquals(Range.valueOf("0"), arity);
+        assertEquals("0", arity.toString());
+    }
+    @Test
+    public void testArityForParameters_intFieldImplicitArity1() throws Exception {
+        class ImplicitSingleField { @Parameters int intSingleValue; }
+        Range arity = Range.parameterArity(ImplicitSingleField.class.getDeclaredField("intSingleValue"));
+        assertEquals(Range.valueOf("1"), arity);
+        assertEquals("1", arity.toString());
+    }
+    @Test
+    public void testArityForParameters_listFieldImplicitArity0_n() throws Exception {
+        Range arity = Range.parameterArity(ListPositionalParams.class.getDeclaredField("list"));
+        assertEquals(Range.valueOf("0..*"), arity);
+        assertEquals("0..*", arity.toString());
+    }
+    @Test
+    public void testArityForParameters_arrayFieldImplicitArity0_n() throws Exception {
+        Range arity = Range.parameterArity(CompactFields.class.getDeclaredField("inputFiles"));
+        assertEquals(Range.valueOf("0..*"), arity);
+        assertEquals("0..*", arity.toString());
+    }
+    @Test
+    public void testArrayOptionsWithArity0_nConsumeAllArguments() {
+        final double[] DEFAULT_PARAMS = new double[] {1, 2};
+        class ArrayOptionsArity0_nAndParameters {
+            @Parameters double[] doubleParams = DEFAULT_PARAMS;
+            @Option(names = "-doubles", arity = "0..*") double[] doubleOptions;
+        }
+        ArrayOptionsArity0_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionsArity0_nAndParameters(), "-doubles 1.1 2.2 3.3 4.4".split(" "));
+        assertArrayEquals(Arrays.toString(params.doubleOptions),
+                new double[] {1.1, 2.2, 3.3, 4.4}, params.doubleOptions, 0.000001);
+        assertArrayEquals(DEFAULT_PARAMS, params.doubleParams, 0.000001);
+    }
+
+    @Test
+    public void testArrayOptionsWithArity1_nConsumeAllArguments() {
+        class ArrayOptionsArity1_nAndParameters {
+            @Parameters double[] doubleParams;
+            @Option(names = "-doubles", arity = "1..*") double[] doubleOptions;
+        }
+        ArrayOptionsArity1_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionsArity1_nAndParameters(), "-doubles 1.1 2.2 3.3 4.4".split(" "));
+        assertArrayEquals(Arrays.toString(params.doubleOptions),
+                new double[] {1.1, 2.2, 3.3, 4.4}, params.doubleOptions, 0.000001);
+        assertArrayEquals(null, params.doubleParams, 0.000001);
+    }
+
+    @Test
+    public void testArrayOptionsWithArity2_nConsumeAllArguments() {
+        class ArrayOptionsArity2_nAndParameters {
+            @Parameters double[] doubleParams;
+            @Option(names = "-doubles", arity = "2..*") double[] doubleOptions;
+        }
+        ArrayOptionsArity2_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionsArity2_nAndParameters(), "-doubles 1.1 2.2 3.3 4.4".split(" "));
+        assertArrayEquals(Arrays.toString(params.doubleOptions),
+                new double[] {1.1, 2.2, 3.3, 4.4}, params.doubleOptions, 0.000001);
+        assertArrayEquals(null, params.doubleParams, 0.000001);
+    }
+
+    @Test
+    public void testArrayOptionArity2_nConsumesAllArgumentsUpToClusteredOption() {
+        class ArrayOptionsArity2_nAndParameters {
+            @Parameters String[] stringParams;
+            @Option(names = "-s", arity = "2..*") String[] stringOptions;
+            @Option(names = "-v") boolean verbose;
+            @Option(names = "-f") File file;
+        }
+        ArrayOptionsArity2_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionsArity2_nAndParameters(), "-s 1.1 2.2 3.3 4.4 -vfFILE 5.5".split(" "));
+        assertArrayEquals(Arrays.toString(params.stringOptions),
+                new String[] {"1.1", "2.2", "3.3", "4.4"}, params.stringOptions);
+        assertTrue(params.verbose);
+        assertEquals(new File("FILE"), params.file);
+        assertArrayEquals(new String[] {"5.5"}, params.stringParams);
+    }
+
+    @Test
+    public void testArrayOptionArity2_nConsumesAllArgumentIncludingQuotedSimpleOption() {
+        class ArrayOptionArity2_nAndParameters {
+            @Parameters String[] stringParams;
+            @Option(names = "-s", arity = "2..*") String[] stringOptions;
+            @Option(names = "-v") boolean verbose;
+            @Option(names = "-f") File file;
+        }
+        ArrayOptionArity2_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArity2_nAndParameters(), "-s 1.1 2.2 3.3 4.4 \"-v\" \"-f\" \"FILE\" 5.5".split(" "));
+        assertArrayEquals(Arrays.toString(params.stringOptions),
+                new String[] {"1.1", "2.2", "3.3", "4.4", "-v", "-f", "FILE", "5.5"}, params.stringOptions);
+        assertFalse("verbose", params.verbose);
+        assertNull("file", params.file);
+        assertArrayEquals(null, params.stringParams);
+    }
+
+    @Test
+    public void testArrayOptionArity2_nConsumesAllArgumentIncludingQuotedClusteredOption() {
+        class ArrayOptionArity2_nAndParameters {
+            @Parameters String[] stringParams;
+            @Option(names = "-s", arity = "2..*") String[] stringOptions;
+            @Option(names = "-v") boolean verbose;
+            @Option(names = "-f") File file;
+        }
+        ArrayOptionArity2_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArity2_nAndParameters(), "-s 1.1 2.2 3.3 4.4 \"-vfFILE\" 5.5".split(" "));
+        assertArrayEquals(Arrays.toString(params.stringOptions),
+                new String[] {"1.1", "2.2", "3.3", "4.4", "-vfFILE", "5.5"}, params.stringOptions);
+        assertFalse("verbose", params.verbose);
+        assertNull("file", params.file);
+        assertArrayEquals(null, params.stringParams);
+    }
+
+    @Test
+    public void testArrayOptionArity2_nConsumesAllArgumentsUpToNextSimpleOption() {
+        class ArrayOptionArity2_nAndParameters {
+            @Parameters double[] doubleParams;
+            @Option(names = "-s", arity = "2..*") String[] stringOptions;
+            @Option(names = "-v") boolean verbose;
+            @Option(names = "-f") File file;
+        }
+        ArrayOptionArity2_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArity2_nAndParameters(), "-s 1.1 2.2 3.3 4.4 -v -f=FILE 5.5".split(" "));
+        assertArrayEquals(Arrays.toString(params.stringOptions),
+                new String[] {"1.1", "2.2", "3.3", "4.4"}, params.stringOptions);
+        assertTrue(params.verbose);
+        assertEquals(new File("FILE"), params.file);
+        assertArrayEquals(new double[] {5.5}, params.doubleParams, 0.000001);
+    }
+
+    @Test
+    public void testArrayOptionArity2_nConsumesAllArgumentsUpToNextOptionWithAttachment() {
+        class ArrayOptionArity2_nAndParameters {
+            @Parameters double[] doubleParams;
+            @Option(names = "-s", arity = "2..*") String[] stringOptions;
+            @Option(names = "-v") boolean verbose;
+            @Option(names = "-f") File file;
+        }
+        ArrayOptionArity2_nAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArity2_nAndParameters(), "-s 1.1 2.2 3.3 4.4 -f=FILE -v 5.5".split(" "));
+        assertArrayEquals(Arrays.toString(params.stringOptions),
+                new String[] {"1.1", "2.2", "3.3", "4.4"}, params.stringOptions);
+        assertTrue(params.verbose);
+        assertEquals(new File("FILE"), params.file);
+        assertArrayEquals(new double[] {5.5}, params.doubleParams, 0.000001);
+    }
+
+    @Test
+    public void testArrayOptionArityNConsumeAllArguments() {
+        class ArrayOptionArityNAndParameters {
+            @Parameters char[] charParams;
+            @Option(names = "-chars", arity = "*") char[] charOptions;
+        }
+        ArrayOptionArityNAndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArityNAndParameters(), "-chars a b c d".split(" "));
+        assertArrayEquals(Arrays.toString(params.charOptions),
+                new char[] {'a', 'b', 'c', 'd'}, params.charOptions);
+        assertArrayEquals(null, params.charParams);
+    }
+
+    private static class BooleanOptionsArity0_nAndParameters {
+        @Parameters String[] params;
+        @Option(names = "-bool", arity = "0..*") boolean bool;
+        @Option(names = {"-v", "-other"}, arity="0..*") boolean vOrOther;
+        @Option(names = "-r") boolean rBoolean;
+    }
+    @Test
+    public void testBooleanOptionsArity0_nConsume1ArgumentIfPossible() { // ignores varargs
+        BooleanOptionsArity0_nAndParameters
+                params = CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-bool false false true".split(" "));
+        assertFalse(params.bool);
+        assertArrayEquals(new String[]{ "false", "true"}, params.params);
+    }
+    @Test
+    public void testBooleanOptionsArity0_nRequiresNoArgument() { // ignores varargs
+        BooleanOptionsArity0_nAndParameters
+                params = CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-bool".split(" "));
+        assertTrue(params.bool);
+    }
+    @Test
+    public void testBooleanOptionsArity0_nConsume0ArgumentsIfNextArgIsOption() { // ignores varargs
+        BooleanOptionsArity0_nAndParameters
+                params = CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-bool -other".split(" "));
+        assertTrue(params.bool);
+        assertTrue(params.vOrOther);
+    }
+    @Test
+    public void testBooleanOptionsArity0_nConsume0ArgumentsIfNextArgIsParameter() { // ignores varargs
+        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);
+    }
+    @Test
+    public void testBooleanOptionsArity0_nFailsIfAttachedParamNotABoolean() { // ignores varargs
+        try {
+            CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-bool=123 -other".split(" "));
+            fail("was able to assign 123 to boolean");
+        } catch (ParameterException ex) {
+            assertEquals("'123' is not a boolean for option '-bool'", ex.getMessage());
+        }
+    }
+    @Test
+    public void testBooleanOptionsArity0_nShortFormFailsIfAttachedParamNotABoolean() { // ignores varargs
+        try {
+            CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-rv234 -bool".split(" "));
+            fail("Expected exception");
+        } catch (UnmatchedArgumentException ok) {
+            assertEquals("Unmatched arguments [-234, -bool]", ok.getMessage());
+        }
+    }
+    @Test
+    public void testBooleanOptionsArity0_nShortFormFailsIfAttachedParamNotABooleanWithUnmatchedArgsAllowed() { // ignores varargs
+        CommandLine cmd = new CommandLine(new BooleanOptionsArity0_nAndParameters()).setUnmatchedArgumentsAllowed(true);
+        cmd.parse("-rv234 -bool".split(" "));
+        assertEquals(Arrays.asList("-234", "-bool"), cmd.getUnmatchedArguments());
+    }
+    @Test
+    public void testBooleanOptionsArity0_nShortFormFailsIfAttachedWithSepParamNotABoolean() { // ignores varargs
+        try {
+            CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-rv=234 -bool".split(" "));
+            fail("was able to assign 234 to boolean");
+        } catch (ParameterException ex) {
+            assertEquals("'234' is not a boolean for option '-v'", ex.getMessage());
+        }
+    }
+
+    private static class BooleanOptionsArity1_nAndParameters {
+        @Parameters boolean[] boolParams;
+        @Option(names = "-bool", arity = "1..*") boolean aBoolean;
+    }
+    @Test
+    public void testBooleanOptionsArity1_nConsume1Argument() { // ignores varargs
+        BooleanOptionsArity1_nAndParameters
+                params = CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool false false true".split(" "));
+        assertFalse(params.aBoolean);
+        assertArrayEquals(new boolean[]{ false, true}, params.boolParams);
+
+        params = CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool true false true".split(" "));
+        assertTrue(params.aBoolean);
+        assertArrayEquals(new boolean[]{ false, true}, params.boolParams);
+    }
+    @Test
+    public void testBooleanOptionsArity1_nCaseInsensitive() { // ignores varargs
+        BooleanOptionsArity1_nAndParameters
+                params = CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool fAlsE false true".split(" "));
+        assertFalse(params.aBoolean);
+        assertArrayEquals(new boolean[]{ false, true}, params.boolParams);
+
+        params = CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool FaLsE false true".split(" "));
+        assertFalse(params.aBoolean);
+        assertArrayEquals(new boolean[]{ false, true}, params.boolParams);
+
+        params = CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool tRuE false true".split(" "));
+        assertTrue(params.aBoolean);
+        assertArrayEquals(new boolean[]{ false, true}, params.boolParams);
+    }
+    @Test
+    public void testBooleanOptionsArity1_nErrorIfValueNotTrueOrFalse() { // ignores varargs
+        try {
+            CommandLine.populateCommand(new BooleanOptionsArity1_nAndParameters(), "-bool abc".split(" "));
+            fail("Invalid format abc was accepted for boolean");
+        } catch (ParameterException expected) {
+            assertEquals("'abc' is not a boolean for option '-bool'", expected.getMessage());
+        }
+    }
+    @Test
+    public void testBooleanOptionsArity1_nErrorIfValueMissing() {
+        try {
+            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());
+        }
+    }
+
+    @Test
+    public void testBooleanOptionArity0Consumes0Arguments() {
+        class BooleanOptionArity0AndParameters {
+            @Parameters boolean[] boolParams;
+            @Option(names = "-bool", arity = "0") boolean aBoolean;
+        }
+        BooleanOptionArity0AndParameters
+                params = CommandLine.populateCommand(new BooleanOptionArity0AndParameters(), "-bool true false true".split(" "));
+        assertTrue(params.aBoolean);
+        assertArrayEquals(new boolean[]{true, false, true}, params.boolParams);
+    }
+
+    @Test
+    public void testIntOptionArity1_nConsumes1Argument() { // ignores varargs
+        class IntOptionArity1_nAndParameters {
+            @Parameters int[] intParams;
+            @Option(names = "-int", arity = "1..*") int anInt;
+        }
+        IntOptionArity1_nAndParameters
+                params = CommandLine.populateCommand(new IntOptionArity1_nAndParameters(), "-int 23 42 7".split(" "));
+        assertEquals(23, params.anInt);
+        assertArrayEquals(new int[]{ 42, 7}, params.intParams);
+    }
+
+    @Test
+    public void testArrayOptionsWithArity0Consume0Arguments() {
+        class OptionsArray0ArityAndParameters {
+            @Parameters double[] doubleParams;
+            @Option(names = "-doubles", arity = "0") double[] doubleOptions;
+        }
+        OptionsArray0ArityAndParameters
+                params = CommandLine.populateCommand(new OptionsArray0ArityAndParameters(), "-doubles 1.1 2.2 3.3 4.4".split(" "));
+        assertArrayEquals(Arrays.toString(params.doubleOptions),
+                new double[0], params.doubleOptions, 0.000001);
+        assertArrayEquals(new double[]{1.1, 2.2, 3.3, 4.4}, params.doubleParams, 0.000001);
+    }
+
+    @Test
+    public void testArrayOptionWithArity1Consumes1Argument() {
+        class Options1ArityAndParameters {
+            @Parameters double[] doubleParams;
+            @Option(names = "-doubles", arity = "1") double[] doubleOptions;
+        }
+        Options1ArityAndParameters
+                params = CommandLine.populateCommand(new Options1ArityAndParameters(), "-doubles 1.1 2.2 3.3 4.4".split(" "));
+        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);
+    }
+
+    private static class ArrayOptionArity2AndParameters {
+        @Parameters double[] doubleParams;
+        @Option(names = "-doubles", arity = "2") double[] doubleOptions;
+    }
+    @Test
+    public void testArrayOptionWithArity2Consumes2Arguments() {
+        ArrayOptionArity2AndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArity2AndParameters(), "-doubles 1.1 2.2 3.3 4.4".split(" "));
+        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);
+    }
+    @Test
+    public void testArrayOptionsWithArity2Consume2ArgumentsEvenIfFirstIsAttached() {
+        ArrayOptionArity2AndParameters
+                params = CommandLine.populateCommand(new ArrayOptionArity2AndParameters(), "-doubles=1.1 2.2 3.3 4.4".split(" "));
+        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);
+    }
+
+    @Test
+    public void testArrayOptionWithoutArityConsumesAllArguments() {
+        class OptionsNoArityAndParameters {
+            @Parameters char[] charParams;
+            @Option(names = "-chars") char[] charOptions;
+        }
+        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);
+    }
+
+    @Test(expected = MissingTypeConverterException.class)
+    public void testMissingTypeConverter() {
+        class MissingConverter {
+            @Option(names = "--socket") Socket socket;
+        }
+        CommandLine.populateCommand(new MissingConverter(), "--socket anyString".split(" "));
+    }
+
+    @Test
+    public void testArrayParametersWithArityMinusOneToN() {
+        class ArrayParamsNegativeArity {
+            @Parameters(arity = "-1..*")
+            List<String> params;
+        }
+        ArrayParamsNegativeArity params = CommandLine.populateCommand(new ArrayParamsNegativeArity(), "a", "b", "c");
+        assertEquals(Arrays.asList("a", "b", "c"), params.params);
+
+        params = CommandLine.populateCommand(new ArrayParamsNegativeArity(), "a");
+        assertEquals(Arrays.asList("a"), params.params);
+
+        params = CommandLine.populateCommand(new ArrayParamsNegativeArity());
+        assertEquals(null, params.params);
+    }
+
+    @Test
+    public void testArrayParametersArity0_n() {
+        class ArrayParamsArity0_n {
+            @Parameters(arity = "0..*")
+            List<String> params;
+        }
+        ArrayParamsArity0_n params = CommandLine.populateCommand(new ArrayParamsArity0_n(), "a", "b", "c");
+        assertEquals(Arrays.asList("a", "b", "c"), params.params);
+
+        params = CommandLine.populateCommand(new ArrayParamsArity0_n(), "a");
+        assertEquals(Arrays.asList("a"), params.params);
+
+        params = CommandLine.populateCommand(new ArrayParamsArity0_n());
+        assertEquals(null, params.params);
+    }
+
+    @Test
+    public void testArrayParametersArity1_n() {
+        class ArrayParamsArity1_n {
+            @Parameters(arity = "1..*")
+            List<String> params;
+        }
+        ArrayParamsArity1_n params = CommandLine.populateCommand(new ArrayParamsArity1_n(), "a", "b", "c");
+        assertEquals(Arrays.asList("a", "b", "c"), params.params);
+
+        params = CommandLine.populateCommand(new ArrayParamsArity1_n(), "a");
+        assertEquals(Arrays.asList("a"), params.params);
+
+        try {
+            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());
+        }
+    }
+
+    @Test
+    public void testArrayParametersArity2_n() {
+        class ArrayParamsArity2_n {
+            @Parameters(arity = "2..*")
+            List<String> params;
+        }
+        ArrayParamsArity2_n params = CommandLine.populateCommand(new ArrayParamsArity2_n(), "a", "b", "c");
+        assertEquals(Arrays.asList("a", "b", "c"), params.params);
+
+        try {
+            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());
+        }
+
+        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());
+        }
+    }
+
+    @Test
+    public void testNonVarargArrayParametersWithNegativeArityConsumesZeroArguments() {
+        class NonVarArgArrayParamsNegativeArity {
+            @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());
+        assertEquals(null, params.params);
+    }
+
+    @Test
+    public void testNonVarargArrayParametersWithArity0() {
+        class NonVarArgArrayParamsZeroArity {
+            @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());
+        assertEquals(null, params.params);
+    }
+
+    @Test
+    public void testNonVarargArrayParametersWithArity1() {
+        class NonVarArgArrayParamsArity1 {
+            @Parameters(arity = "1")
+            List<String> params;
+        }
+        NonVarArgArrayParamsArity1 params = CommandLine.populateCommand(new NonVarArgArrayParamsArity1(), "a", "b", "c");
+        assertEquals(Arrays.asList("a"), params.params);
+
+        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());
+        }
+    }
+
+    @Test
+    public void testNonVarargArrayParametersWithArity2() {
+        class NonVarArgArrayParamsArity2 {
+            @Parameters(arity = "2")
+            List<String> params;
+        }
+        NonVarArgArrayParamsArity2 params = CommandLine.populateCommand(new NonVarArgArrayParamsArity2(), "a", "b", "c");
+        assertEquals(Arrays.asList("a", "b"), params.params);
+
+        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());
+        }
+
+        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());
+        }
+    }
+
+    @Test
+    public void testParametersDeclaredOutOfOrderWithNoArgs() {
+        class WithParams {
+            @Parameters(index = "1") String param1;
+            @Parameters(index = "0") String param0;
+        }
+        try {
+            CommandLine.populateCommand(new WithParams(), new String[0]);
+        } catch (MissingParameterException ex) {
+            assertEquals("Missing required parameters: param0, param1", ex.getMessage());
+        }
+    }
+
+    class VariousPrefixCharacters {
+        @Option(names = {"-d", "--dash"}) int dash;
+        @Option(names = {"/S"}) int slashS;
+        @Option(names = {"/T"}) int slashT;
+        @Option(names = {"/4"}) boolean fourDigit;
+        @Option(names = {"/Owner", "--owner"}) String owner;
+        @Option(names = {"-SingleDash"}) boolean singleDash;
+        @Option(names = {"[CPM"}) String cpm;
+        @Option(names = {"(CMS"}) String cms;
+    }
+    @Test
+    public void testOptionsMayDefineAnyPrefixChar() {
+        VariousPrefixCharacters params = CommandLine.populateCommand(new VariousPrefixCharacters(),
+                "-d 123 /4 /S 765 /T=98 /Owner=xyz -SingleDash [CPM CP/M (CMS=cmsVal".split(" "));
+        assertEquals("-d", 123, params.dash);
+        assertEquals("/S", 765, params.slashS);
+        assertEquals("/T", 98, params.slashT);
+        assertTrue("/4", params.fourDigit);
+        assertTrue("-SingleDash", params.singleDash);
+        assertEquals("/Owner", "xyz", params.owner);
+        assertEquals("[CPM", "CP/M", params.cpm);
+        assertEquals("(CMS", "cmsVal", params.cms);
+    }
+    @Test
+    public void testGnuLongOptionsWithVariousSeparators() {
+        VariousPrefixCharacters params = CommandLine.populateCommand(new VariousPrefixCharacters(), "--dash 123".split(" "));
+        assertEquals("--dash val", 123, params.dash);
+
+        params = CommandLine.populateCommand(new VariousPrefixCharacters(), "--dash=234 --owner=x".split(" "));
+        assertEquals("--dash=val", 234, params.dash);
+        assertEquals("--owner=x", "x", params.owner);
+
+        params = new VariousPrefixCharacters();
+        CommandLine cmd = new CommandLine(params);
+        cmd.setSeparator(":");
+        cmd.parse("--dash:345");
+        assertEquals("--dash:val", 345, params.dash);
+
+        params = new VariousPrefixCharacters();
+        cmd = new CommandLine(params);
+        cmd.setSeparator(":");
+        cmd.parse("--dash:345 --owner:y".split(" "));
+        assertEquals("--dash:val", 345, params.dash);
+        assertEquals("--owner:y", "y", params.owner);
+    }
+    @Test
+    public void testSeparatorCanBeSetDeclaratively() {
+        @Command(separator = ":")
+        class App {
+            @Option(names = "--opt", required = true) String opt;
+        }
+        try {
+            CommandLine.populateCommand(new App(), "--opt=abc");
+            fail("Expected failure with unknown separator");
+        } catch (UnmatchedArgumentException ok) {
+            assertEquals("Unmatched argument [--opt=abc]", ok.getMessage());
+        }
+    }
+    @Test
+    public void testIfSeparatorSetTheDefaultSeparatorIsNotRecognized() {
+        @Command(separator = ":")


<TRUNCATED>