[Git][java-team/picocli][upstream] Import Upstream version 3.9.6

Tony Mancill gitlab at salsa.debian.org
Thu Apr 25 05:25:28 BST 2019



Tony Mancill pushed to branch upstream at Debian Java Maintainers / picocli


Commits:
28847540 by tony mancill at 2019-04-24T03:34:29Z
Import Upstream version 3.9.6
- - - - -


29 changed files:

- README.md
- RELEASE-NOTES.md
- build.gradle
- gradle.properties
- picocli-codegen/README.md
- picocli-codegen/src/main/java/picocli/codegen/aot/graalvm/ReflectionConfigGenerator.java
- picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Example.java
- + picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622AbstractCommand.java
- + picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622App.java
- + picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622Command1.java
- + picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622Command2.java
- + picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622Command2Sub.java
- picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/ReflectionConfigGeneratorTest.java
- picocli-codegen/src/test/resources/example-reflect.json
- + picocli-codegen/src/test/resources/issue622-reflect.json
- + picocli-examples/src/main/java/picocli/examples/interactive/PasswordDemo.java
- src/main/java/picocli/CommandLine.java
- src/test/java/picocli/AutoCompleteTest.java
- src/test/java/picocli/CommandLineArityTest.java
- src/test/java/picocli/CommandLineCommandMethodTest.java
- src/test/java/picocli/CommandLineDefaultProviderTest.java
- src/test/java/picocli/CommandLineHelpTest.java
- src/test/java/picocli/CommandLineMixinTest.java
- src/test/java/picocli/CommandLineTest.java
- src/test/java/picocli/CommandLineTypeConversionTest.java
- + src/test/java/picocli/InteractiveArgTest.java
- src/test/java/picocli/LenientParsingTest.java
- src/test/java/picocli/ModelOptionSpecTest.java
- src/test/java/picocli/ModelPositionalParamSpecTest.java


Changes:

=====================================
README.md
=====================================
@@ -45,7 +45,7 @@ Picocli-based applications can easily [integrate](https://picocli.info/#_depende
 ![Picocli Demo help message with ANSI colors](docs/images/picocli.Demo.png?raw=true)
 
 ### Releases
-* [Releases](https://github.com/remkop/picocli/releases) - Latest: 3.9.2 [Release Notes](https://github.com/remkop/picocli/releases/tag/v3.9.2)
+* [Releases](https://github.com/remkop/picocli/releases) - Latest: 3.9.6 [Release Notes](https://github.com/remkop/picocli/releases/tag/v3.9.6)
 * Older: Picocli 3.0.0 [Release Notes](https://github.com/remkop/picocli/releases/tag/v3.0.0)
 * Older: Picocli 2.0 [Release Notes](https://github.com/remkop/picocli/releases/tag/v2.0.0)
 
@@ -181,35 +181,35 @@ See the [source code](https://github.com/remkop/picocli/blob/master/src/main/jav
 
 ### Gradle
 ```
-compile 'info.picocli:picocli:3.9.2'
+compile 'info.picocli:picocli:3.9.6'
 ```
 ### Maven
 ```
 <dependency>
   <groupId>info.picocli</groupId>
   <artifactId>picocli</artifactId>
-  <version>3.9.2</version>
+  <version>3.9.6</version>
 </dependency>
 ```
 ### Scala SBT
 ```
-libraryDependencies += "info.picocli" % "picocli" % "3.9.2"
+libraryDependencies += "info.picocli" % "picocli" % "3.9.6"
 ```
 ### Ivy
 ```
-<dependency org="info.picocli" name="picocli" rev="3.9.2" />
+<dependency org="info.picocli" name="picocli" rev="3.9.6" />
 ```
 ### Grape
 ```groovy
 @Grapes(
-    @Grab(group='info.picocli', module='picocli', version='3.9.2')
+    @Grab(group='info.picocli', module='picocli', version='3.9.6')
 )
 ```
 ### Leiningen
 ```
-[info.picocli/picocli "3.9.2"]
+[info.picocli/picocli "3.9.6"]
 ```
 ### Buildr
 ```
-'info.picocli:picocli:jar:3.9.2'
+'info.picocli:picocli:jar:3.9.6'
 ```


=====================================
RELEASE-NOTES.md
=====================================
@@ -1,5 +1,169 @@
 # picocli Release Notes
 
+# <a name="3.9.6"></a> Picocli 3.9.6
+The picocli community is pleased to announce picocli 3.9.6.
+
+This release improves support for interactive (password) options:
+
+* interactive options can now use type `char[]` instead of String, to allow applications to null out the array after use so that sensitive information is no longer resident in memory
+* interactive options can be optionally interactive if configured with `arity = "0..1"`
+
+This is the fifty-second public release.
+Picocli follows [semantic versioning](http://semver.org/).
+
+## <a name="3.9.6"></a> Table of Contents
+* [New and noteworthy](#3.9.6-new)
+* [Fixed issues](#3.9.6-fixes)
+* [Deprecations](#3.9.6-deprecated)
+* [Potential breaking changes](#3.9.6-breaking-changes)
+
+## <a name="3.9.6-new"></a> New and Noteworthy
+
+This release improves support for interactive (password) options:
+
+* interactive options can now use type `char[]` instead of String, to allow applications to null out the array after use so that sensitive information is no longer resident in memory
+* interactive options can be optionally interactive if configured with `arity = "0..1"`
+
+
+For example, if an application has these options:
+
+```java
+ at Option(names = "--user")
+String user;
+
+ at Option(names = "--password", arity = "0..1", interactive = true)
+char[] password;
+```
+
+With the following input, the `password` field will be initialized to `"123"` without prompting the user for input:
+
+```
+--password 123 --user Joe
+```
+
+However, if the password is not specified, the user will be prompted to enter a value. In the following example, the password option has no parameter, so the user will be prompted to type in a value on the console:
+
+```
+--password --user Joe
+```
+
+## <a name="3.9.6-fixes"></a> Fixed issues
+* [#657] Support type `char[]` for interactive options. Thanks to [Lukáš Petrovický](https://github.com/triceo) for raising this issue.
+* [#536] Support optionally interactive options. Thanks to [Lukas Heumos](https://github.com/Zethson) for raising this issue.
+
+## <a name="3.9.6-deprecated"></a> Deprecations
+No features were deprecated in this release.
+
+## <a name="3.9.6-breaking-changes"></a> Potential breaking changes
+This release has no breaking changes.
+
+
+
+# <a name="3.9.5"></a> Picocli 3.9.5
+The picocli community is pleased to announce picocli 3.9.5.
+
+This release contains a critical workaround to protect against JVM crashes when running on RedHat Linux 3.10.0-327.44.2.el7.x86_64.
+
+Picocli 3.9.0 introduced a change in the heuristics for emitting ANSI escape characters. As part of this change, picocli may load the `org.fusesource.jansi.AnsiConsole` class from the JAnsi library when not running on Windows. This may crash the JVM (see [fusesource/jansi-native#17](https://github.com/fusesource/jansi-native/issues/17)).
+
+The workaround in this release is to only load the `AnsiConsole` class when running on Windows.
+
+Users using 3.9.0 and higher are strongly recommended to upgrade to 3.9.5 or later.
+
+
+This is the fiftieth public release.
+Picocli follows [semantic versioning](http://semver.org/).
+
+## <a name="3.9.5"></a> Table of Contents
+* [New and noteworthy](#3.9.5-new)
+* [Fixed issues](#3.9.5-fixes)
+* [Deprecations](#3.9.5-deprecated)
+* [Potential breaking changes](#3.9.5-breaking-changes)
+
+## <a name="3.9.5-new"></a> New and Noteworthy
+
+
+## <a name="3.9.5-fixes"></a> Fixed issues
+- [#630] Avoid loading `org.fusesource.jansi.AnsiConsole` when not running on Windows to avoid JVM crashes on non-Windows platforms.
+- [#632] ReflectionConfigGenerator now specifies the `allowWrite = true` attribute for final fields.
+
+## <a name="3.9.5-deprecated"></a> Deprecations
+No features were deprecated in this release.
+
+## <a name="3.9.5-breaking-changes"></a> Potential breaking changes
+This release has no breaking changes.
+
+
+# <a name="3.9.4"></a> Picocli 3.9.4
+The picocli community is pleased to announce picocli 3.9.4.
+
+This release contains bugfixes and enhancements.
+
+From this release, `enum`-typed options and positional parameters that are multi-value can be stored in `EnumSet` collections (in addition to other Collections, arrays and Maps). 
+
+Also, a better error message is now shown when unknown options are encountered while processing clustered short options. The new error message includes both the failing part and the original command line argument.
+
+Bugfixes: 
+* `ReflectionConfigGenerator` incorrectly listed superclass fields as fields of the concrete subclass, causing "GraalVM error: Error parsing reflection configuration in json" when creating a native image.
+* Method subcommands in commands that subclass another command caused `InitializationException`.
+
+
+This is the forty-nineth public release.
+Picocli follows [semantic versioning](http://semver.org/).
+
+## <a name="3.9.4"></a> Table of Contents
+* [New and noteworthy](#3.9.4-new)
+* [Fixed issues](#3.9.4-fixes)
+* [Deprecations](#3.9.4-deprecated)
+* [Potential breaking changes](#3.9.4-breaking-changes)
+
+## <a name="3.9.4-new"></a> New and Noteworthy
+
+
+## <a name="3.9.4-fixes"></a> Fixed issues
+- [#628] Add support for collecting `enum` multi-value options and positional parameters in `EnumSet<>` collections. Thanks to [Lee Atkinson](https://github.com/leeatkinson) for raising this.
+- [#619] Bugfix: Method subcommands in commands that subclass another command caused `InitializationException`: "Another subcommand named 'method' already exists...". Thanks to [PorygonZRocks](https://github.com/PorygonZRocks) for the bug report.
+- [#622] Bugfix: `ReflectionConfigGenerator` incorrectly listed superclass fields as fields of the concrete subclass, causing "GraalVM error: Error parsing reflection configuration in json". Thanks to [Sebastian Thomschke](https://github.com/sebthom) for the bug report.
+- [#623] `ReflectionConfigGenerator` now generates json in alphabetic order.
+- [#627] Improve error message for unknown options when processing clustered short options. 
+
+## <a name="3.9.4-deprecated"></a> Deprecations
+No features were deprecated in this release.
+
+## <a name="3.9.4-breaking-changes"></a> Potential breaking changes
+This release has no breaking changes.
+
+
+
+# <a name="3.9.3"></a> Picocli 3.9.3
+The picocli community is pleased to announce picocli 3.9.3.
+
+This release contains bugfixes and enhancements.
+
+
+This is the forty-eight public release.
+Picocli follows [semantic versioning](http://semver.org/).
+
+## <a name="3.9.3"></a> Table of Contents
+* [New and noteworthy](#3.9.3-new)
+* [Fixed issues](#3.9.3-fixes)
+* [Deprecations](#3.9.3-deprecated)
+* [Potential breaking changes](#3.9.3-breaking-changes)
+
+## <a name="3.9.3-new"></a> New and Noteworthy
+
+
+## <a name="3.9.3-fixes"></a> Fixed issues
+- [#613] Enhancement: Improve picocli heuristics for unmatched options: single-character arguments that don't exactly match options (like `-`) should be considered positional parameters. Thanks to [Oliver Weiler](https://github.com/helpermethod) for the bug report.
+- [#615] Bugfix: Opaque stacktrace for "%" in Option description. Thanks to [petermr](https://github.com/petermr) for the bug report.
+- [#616] Bugfix: showDefaultValues=true with defaultValueProvider did not render defaultValues in usage help. Thanks to [Sebastian Thomschke](https://github.com/sebthom) for the bug report.
+
+## <a name="3.9.3-deprecated"></a> Deprecations
+No features were deprecated in this release.
+
+## <a name="3.9.3-breaking-changes"></a> Potential breaking changes
+This release has no breaking changes.
+
 
 # <a name="3.9.2"></a> Picocli 3.9.2
 The picocli community is pleased to announce picocli 3.9.2.


=====================================
build.gradle
=====================================
@@ -331,9 +331,8 @@ Release procedure:
 
 (When releasing from branch)
 25. Switch to master
-26. Update RELEASE-NOTES.md (insert changes from branch)
-27. Update last release version in README
-28. Update `projectPreviousVersion` in build.gradle
-29. gradlew copyDocs
-30. commit -m "Update master for release x.x (from branch x.x)"
+26. cherry-pick the "Release picocli version ..." commit
+27. gradlew bumpVersion
+28. check modified files
+29. commit -m "Update master for next development cycle after release x.x (from branch x.x)"
 */


=====================================
gradle.properties
=====================================
@@ -11,12 +11,12 @@ junitDepVersion     = 4.11
 junitVersion        = 4.12
 
 # projectPreviousReleaseVersion is non-SNAPSHOT, only published releases
-projectPreviousReleaseVersion = 3\\.9\\.1
+projectPreviousReleaseVersion = 3\\.9\\.5
 # projectPreviousVersionRegex may be a SNAPSHOT
-projectPreviousVersionRegex   = 3\\.9\\.2-SNAPSHOT
-projectVersion                = 3.9.2
+projectPreviousVersionRegex   = 3\\.9\\.6-SNAPSHOT
+projectVersion                = 3.9.6
 
-releaseDate              = 2019-01-20
-releaseDatePreviousRegex = 2019\\-01\\-10
+releaseDate              = 2019-04-06
+releaseDatePreviousRegex = 2019\\-02\\-18
 
 systemRulesVersion = 1.17.1
\ No newline at end of file


=====================================
picocli-codegen/README.md
=====================================
@@ -63,7 +63,7 @@ Note that the `picocli-codegen` module is only added as a dependency for the `ex
         <dependency>
           <groupId>info.picocli</groupId>
           <artifactId>picocli-codegen</artifactId>
-          <version>3.9.2</version>
+          <version>3.9.6</version>
           <type>jar</type>
         </dependency>
       </dependencies>
@@ -82,8 +82,8 @@ configurations {
     generateConfig
 }
 dependencies {
-    compile 'info.picocli:picocli:3.9.2'
-    generateConfig 'info.picocli:picocli-codegen:3.9.2'
+    compile 'info.picocli:picocli:3.9.6'
+    generateConfig 'info.picocli:picocli-codegen:3.9.6'
 }
 ```
 


=====================================
picocli-codegen/src/main/java/picocli/codegen/aot/graalvm/ReflectionConfigGenerator.java
=====================================
@@ -17,14 +17,9 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.lang.reflect.Proxy;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.Callable;
 
 /**
@@ -138,13 +133,20 @@ public class ReflectionConfigGenerator {
     }
 
     static final class Visitor {
-        Map<String, ReflectedClass> visited = new LinkedHashMap<String, ReflectedClass>();
+        Map<String, ReflectedClass> visited = new TreeMap<String, ReflectedClass>();
 
         Visitor() {
             getOrCreateClass(Method.class);
             getOrCreateClassName("java.lang.reflect.Executable").addMethod("getParameters");
             getOrCreateClassName("java.lang.reflect.Parameter").addMethod("getName");
 
+            // ANSI color enabled detection
+            getOrCreateClassName("java.lang.System").addMethod("console");
+            getOrCreateClassName("org.fusesource.jansi.AnsiConsole").addField("out", false);
+
+            // picocli 4.0
+            getOrCreateClassName("java.util.ResourceBundle").addMethod("getBaseBundleName");
+
             // type converters registered with reflection
             getOrCreateClassName("java.time.Duration").addMethod("parse", CharSequence.class);
             getOrCreateClassName("java.time.Instant").addMethod("parse", CharSequence.class);
@@ -212,21 +214,25 @@ public class ReflectionConfigGenerator {
             Field[] declaredFields = cls.getDeclaredFields();
             for (Field f : declaredFields) {
                 if (f.isAnnotationPresent(CommandLine.Spec.class)) {
-                    reflectedClass.addField(f.getName());
+                    reflectedClass.addField(f.getName(), isFinal(f));
                 }
                 if (f.isAnnotationPresent(CommandLine.ParentCommand.class)) {
-                    reflectedClass.addField(f.getName());
+                    reflectedClass.addField(f.getName(), isFinal(f));
                 }
                 if (f.isAnnotationPresent(CommandLine.Mixin.class)) {
-                    reflectedClass.addField(f.getName());
+                    reflectedClass.addField(f.getName(), isFinal(f));
                 }
                 if (f.isAnnotationPresent(CommandLine.Unmatched.class)) {
-                    reflectedClass.addField(f.getName());
+                    reflectedClass.addField(f.getName(), isFinal(f));
                 }
             }
             visitAnnotatedFields(cls.getSuperclass());
         }
 
+        private boolean isFinal(Field f) {
+            return (f.getModifiers() & Modifier.FINAL) == Modifier.FINAL;
+        }
+
         private void visitArgSpec(ArgSpec argSpec) throws NoSuchFieldException, IllegalAccessException {
             visitGetter(argSpec.getter());
             visitSetter(argSpec.setter());
@@ -280,8 +286,11 @@ public class ReflectionConfigGenerator {
 
         private void visitFieldBinding(Object fieldBinding) throws IllegalAccessException, NoSuchFieldException {
             Field field = (Field) accessibleField(fieldBinding.getClass(), REFLECTED_FIELD_BINDING_FIELD).get(fieldBinding);
+            getOrCreateClass(field.getDeclaringClass())
+                    .addField(field.getName(), isFinal(field));
+
             Object scope = accessibleField(fieldBinding.getClass(), REFLECTED_BINDING_FIELD_SCOPE).get(fieldBinding);
-            getOrCreateClass(scope.getClass()).addField(field.getName());
+            getOrCreateClass(scope.getClass());
         }
 
         private void visitMethodBinding(Object methodBinding) throws IllegalAccessException, NoSuchFieldException {
@@ -332,15 +341,15 @@ public class ReflectionConfigGenerator {
     }
     static class ReflectedClass {
         private final String name;
-        private final Set<ReflectedField> fields = new LinkedHashSet<ReflectedField>();
-        private final Set<ReflectedMethod> methods = new LinkedHashSet<ReflectedMethod>();
+        private final Set<ReflectedField> fields = new TreeSet<ReflectedField>();
+        private final Set<ReflectedMethod> methods = new TreeSet<ReflectedMethod>();
 
         ReflectedClass(String name) {
             this.name = name;
         }
 
-        ReflectedClass addField(String fieldName) {
-            fields.add(new ReflectedField(fieldName));
+        ReflectedClass addField(String fieldName, boolean isFinal) {
+            fields.add(new ReflectedField(fieldName, isFinal));
             return this;
         }
 
@@ -388,7 +397,7 @@ public class ReflectionConfigGenerator {
             return result;
         }
     }
-    static class ReflectedMethod {
+    static class ReflectedMethod implements Comparable<ReflectedMethod> {
         private final String name;
         private final String[] paramTypes;
 
@@ -418,12 +427,22 @@ public class ReflectionConfigGenerator {
             }
             return result.toString();
         }
+
+        public int compareTo(ReflectedMethod o) {
+            int result = name.compareTo(o.name);
+            if (result == 0) {
+                result = Arrays.toString(this.paramTypes).compareTo(Arrays.toString(o.paramTypes));
+            }
+            return result;
+        }
     }
-    static class ReflectedField {
+    static class ReflectedField implements Comparable<ReflectedField> {
         private final String name;
+        private final boolean isFinal;
 
-        ReflectedField(String name) {
+        ReflectedField(String name, boolean isFinal) {
             this.name = name;
+            this.isFinal = isFinal;
         }
         @Override public int hashCode() { return name.hashCode(); }
         @Override public boolean equals(Object o) {
@@ -432,7 +451,13 @@ public class ReflectionConfigGenerator {
 
         @Override
         public String toString() {
-            return String.format("{ \"name\" : \"%s\" }", name);
+            return isFinal
+                    ? String.format("{ \"name\" : \"%s\", \"allowWrite\" : true }", name)
+                    : String.format("{ \"name\" : \"%s\" }", name);
+        }
+
+        public int compareTo(ReflectedField o) {
+            return name.compareTo(o.name);
         }
     }
 }


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Example.java
=====================================
@@ -11,6 +11,7 @@ import picocli.CommandLine.Spec;
 import picocli.CommandLine.Unmatched;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -26,7 +27,7 @@ public class Example implements Runnable {
     }
 
     @Option(names = "-t")
-    TimeUnit timeUnit;
+    final TimeUnit timeUnit = TimeUnit.SECONDS;
 
     @Parameters(index = "0")
     File file;
@@ -38,10 +39,10 @@ public class Example implements Runnable {
     ExampleMixin mixin;
 
     @Unmatched
-    List<String> unmatched;
+    final List<String> unmatched = new ArrayList<String>();
 
     private int minimum;
-    private File[] otherFiles;
+    private List<File> otherFiles;
 
     @Command
     int multiply(@Option(names = "--count") int count,
@@ -59,7 +60,7 @@ public class Example implements Runnable {
     }
 
     @Parameters(index = "1..*")
-    public void setOtherFiles(File[] otherFiles) {
+    public void setOtherFiles(List<File> otherFiles) {
         for (File f : otherFiles) {
             if (!f.exists()) {
                 throw new ParameterException(spec.commandLine(), "File " + f.getAbsolutePath() + " must exist");
@@ -70,7 +71,7 @@ public class Example implements Runnable {
 
     public void run() {
         System.out.printf("timeUnit=%s, length=%s, file=%s, unmatched=%s, minimum=%s, otherFiles=%s%n",
-                timeUnit, mixin.length, file, unmatched, minimum, Arrays.toString(otherFiles));
+                timeUnit, mixin.length, file, unmatched, minimum, otherFiles.toString());
     }
 
     public static void main(String[] args) {


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622AbstractCommand.java
=====================================
@@ -0,0 +1,14 @@
+package picocli.codegen.aot.graalvm;
+
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+
+import java.io.File;
+
+public class Issue622AbstractCommand {
+    @Option(names = "-v")
+    boolean verbose;
+
+    @Parameters
+    File file;
+}


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622App.java
=====================================
@@ -0,0 +1,7 @@
+package picocli.codegen.aot.graalvm;
+
+import picocli.CommandLine.Command;
+
+ at Command(name = "app", subcommands = {Issue622Command1.class, Issue622Command2.class})
+public class Issue622App extends Issue622AbstractCommand {
+}


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622Command1.java
=====================================
@@ -0,0 +1,7 @@
+package picocli.codegen.aot.graalvm;
+
+import picocli.CommandLine.Command;
+
+ at Command(name = "cmd1")
+public class Issue622Command1 extends Issue622AbstractCommand {
+}


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622Command2.java
=====================================
@@ -0,0 +1,10 @@
+package picocli.codegen.aot.graalvm;
+
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+ at Command(name = "cmd2", subcommands = Issue622Command2Sub.class)
+public class Issue622Command2 extends Issue622AbstractCommand {
+    @Option(names = "-x")
+    int x;
+}


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/Issue622Command2Sub.java
=====================================
@@ -0,0 +1,10 @@
+package picocli.codegen.aot.graalvm;
+
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+ at Command(name = "sub")
+public class Issue622Command2Sub extends Issue622AbstractCommand {
+    @Option(names = "-x")
+    int x;
+}


=====================================
picocli-codegen/src/test/java/picocli/codegen/aot/graalvm/ReflectionConfigGeneratorTest.java
=====================================
@@ -41,6 +41,23 @@ public class ReflectionConfigGeneratorTest {
         expected = expected.replace("\n", System.getProperty("line.separator"));
 
         String actual = readAndClose(new FileInputStream(file));
+        file.delete();
+
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testIssue622FieldsFromAbstractSuperclass() throws IOException {
+        File file = File.createTempFile("picocli-codegen", ".json");
+
+        ReflectionConfigGenerator.main("--output", file.getAbsolutePath(), Issue622App.class.getName());
+
+        String expected = read("/issue622-reflect.json");
+        expected = expected.replace("\r\n", "\n");
+        expected = expected.replace("\n", System.getProperty("line.separator"));
+
+        String actual = readAndClose(new FileInputStream(file));
+        file.delete();
 
         assertEquals(expected, actual);
     }


=====================================
picocli-codegen/src/test/resources/example-reflect.json
=====================================
@@ -1,11 +1,42 @@
 [
   {
-    "name" : "java.lang.reflect.Method",
+    "name" : "[Ljava.lang.String;",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.io.File",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.lang.Object",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.lang.String",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true
   },
+  {
+    "name" : "java.lang.System",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "console", "parameterTypes" : [] }
+    ]
+  },
   {
     "name" : "java.lang.reflect.Executable",
     "allDeclaredConstructors" : true,
@@ -16,6 +47,13 @@
       { "name" : "getParameters", "parameterTypes" : [] }
     ]
   },
+  {
+    "name" : "java.lang.reflect.Method",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
   {
     "name" : "java.lang.reflect.Parameter",
     "allDeclaredConstructors" : true,
@@ -27,67 +65,59 @@
     ]
   },
   {
-    "name" : "java.time.Duration",
+    "name" : "java.nio.file.Path",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true,
-    "methods" : [
-      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
-    ]
+    "allPublicMethods" : true
   },
   {
-    "name" : "java.time.Instant",
+    "name" : "java.nio.file.Paths",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+      { "name" : "get", "parameterTypes" : ["java.lang.String", "[Ljava.lang.String;"] }
     ]
   },
   {
-    "name" : "java.time.LocalDate",
+    "name" : "java.sql.Connection",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true,
-    "methods" : [
-      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
-    ]
+    "allPublicMethods" : true
   },
   {
-    "name" : "java.time.LocalDateTime",
+    "name" : "java.sql.Driver",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true,
-    "methods" : [
-      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
-    ]
+    "allPublicMethods" : true
   },
   {
-    "name" : "java.time.LocalTime",
+    "name" : "java.sql.DriverManager",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+      { "name" : "getConnection", "parameterTypes" : ["java.lang.String"] },
+      { "name" : "getDriver", "parameterTypes" : ["java.lang.String"] }
     ]
   },
   {
-    "name" : "java.time.MonthDay",
+    "name" : "java.sql.Timestamp",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+      { "name" : "valueOf", "parameterTypes" : ["java.lang.String"] }
     ]
   },
   {
-    "name" : "java.time.OffsetDateTime",
+    "name" : "java.time.Duration",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
@@ -97,7 +127,7 @@
     ]
   },
   {
-    "name" : "java.time.OffsetTime",
+    "name" : "java.time.Instant",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
@@ -107,7 +137,7 @@
     ]
   },
   {
-    "name" : "java.time.Period",
+    "name" : "java.time.LocalDate",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
@@ -117,7 +147,7 @@
     ]
   },
   {
-    "name" : "java.time.Year",
+    "name" : "java.time.LocalDateTime",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
@@ -127,7 +157,7 @@
     ]
   },
   {
-    "name" : "java.time.YearMonth",
+    "name" : "java.time.LocalTime",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
@@ -137,7 +167,7 @@
     ]
   },
   {
-    "name" : "java.time.ZonedDateTime",
+    "name" : "java.time.MonthDay",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
@@ -147,125 +177,124 @@
     ]
   },
   {
-    "name" : "java.time.ZoneId",
+    "name" : "java.time.OffsetDateTime",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "of", "parameterTypes" : ["java.lang.String"] }
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
     ]
   },
   {
-    "name" : "java.time.ZoneOffset",
+    "name" : "java.time.OffsetTime",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "of", "parameterTypes" : ["java.lang.String"] }
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
     ]
   },
   {
-    "name" : "java.nio.file.Path",
+    "name" : "java.time.Period",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
   },
   {
-    "name" : "java.nio.file.Paths",
+    "name" : "java.time.Year",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "get", "parameterTypes" : ["java.lang.String", "[Ljava.lang.String;"] }
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
     ]
   },
   {
-    "name" : "java.sql.Connection",
+    "name" : "java.time.YearMonth",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
   },
   {
-    "name" : "java.sql.Driver",
+    "name" : "java.time.ZoneId",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "of", "parameterTypes" : ["java.lang.String"] }
+    ]
   },
   {
-    "name" : "java.sql.DriverManager",
+    "name" : "java.time.ZoneOffset",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "getConnection", "parameterTypes" : ["java.lang.String"] },
-      { "name" : "getDriver", "parameterTypes" : ["java.lang.String"] }
+      { "name" : "of", "parameterTypes" : ["java.lang.String"] }
     ]
   },
   {
-    "name" : "java.sql.Timestamp",
+    "name" : "java.time.ZonedDateTime",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "methods" : [
-      { "name" : "valueOf", "parameterTypes" : ["java.lang.String"] }
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
     ]
   },
   {
-    "name" : "picocli.codegen.aot.graalvm.Example",
+    "name" : "java.util.Collections$UnmodifiableRandomAccessList",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true,
-    "fields" : [
-      { "name" : "spec" },
-      { "name" : "mixin" },
-      { "name" : "unmatched" },
-      { "name" : "timeUnit" },
-      { "name" : "file" }
-    ],
-    "methods" : [
-      { "name" : "setMinimum", "parameterTypes" : ["int"] },
-      { "name" : "setOtherFiles", "parameterTypes" : ["[Ljava.io.File;"] },
-      { "name" : "multiply", "parameterTypes" : ["int", "int"] }
-    ]
+    "allPublicMethods" : true
   },
   {
-    "name" : "java.lang.Object",
+    "name" : "java.util.List",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true
   },
   {
-    "name" : "java.util.concurrent.TimeUnit",
+    "name" : "java.util.ResourceBundle",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "getBaseBundleName", "parameterTypes" : [] }
+    ]
   },
   {
-    "name" : "java.util.Collections$UnmodifiableRandomAccessList",
+    "name" : "java.util.concurrent.TimeUnit",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true
   },
   {
-    "name" : "picocli.codegen.aot.graalvm.Example$ExampleMixin",
+    "name" : "org.fusesource.jansi.AnsiConsole",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "fields" : [
-      { "name" : "length" }
+      { "name" : "out" }
     ]
   },
   {
@@ -279,20 +308,6 @@
       { "name" : "versionRequested" }
     ]
   },
-  {
-    "name" : "java.io.File",
-    "allDeclaredConstructors" : true,
-    "allPublicConstructors" : true,
-    "allDeclaredMethods" : true,
-    "allPublicMethods" : true
-  },
-  {
-    "name" : "[Ljava.io.File;",
-    "allDeclaredConstructors" : true,
-    "allPublicConstructors" : true,
-    "allDeclaredMethods" : true,
-    "allPublicMethods" : true
-  },
   {
     "name" : "picocli.CommandLine$HelpCommand",
     "allDeclaredConstructors" : true,
@@ -300,22 +315,37 @@
     "allDeclaredMethods" : true,
     "allPublicMethods" : true,
     "fields" : [
-      { "name" : "helpRequested" },
-      { "name" : "commands" }
+      { "name" : "commands" },
+      { "name" : "helpRequested" }
     ]
   },
   {
-    "name" : "[Ljava.lang.String;",
+    "name" : "picocli.codegen.aot.graalvm.Example",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true
+    "allPublicMethods" : true,
+    "fields" : [
+      { "name" : "file" },
+      { "name" : "mixin" },
+      { "name" : "spec" },
+      { "name" : "timeUnit", "allowWrite" : true },
+      { "name" : "unmatched", "allowWrite" : true }
+    ],
+    "methods" : [
+      { "name" : "multiply", "parameterTypes" : ["int", "int"] },
+      { "name" : "setMinimum", "parameterTypes" : ["int"] },
+      { "name" : "setOtherFiles", "parameterTypes" : ["java.util.List"] }
+    ]
   },
   {
-    "name" : "java.lang.String",
+    "name" : "picocli.codegen.aot.graalvm.Example$ExampleMixin",
     "allDeclaredConstructors" : true,
     "allPublicConstructors" : true,
     "allDeclaredMethods" : true,
-    "allPublicMethods" : true
+    "allPublicMethods" : true,
+    "fields" : [
+      { "name" : "length" }
+    ]
   }
 ]


=====================================
picocli-codegen/src/test/resources/issue622-reflect.json
=====================================
@@ -0,0 +1,310 @@
+[
+  {
+    "name" : "java.io.File",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.lang.Object",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.lang.System",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "console", "parameterTypes" : [] }
+    ]
+  },
+  {
+    "name" : "java.lang.reflect.Executable",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "getParameters", "parameterTypes" : [] }
+    ]
+  },
+  {
+    "name" : "java.lang.reflect.Method",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.lang.reflect.Parameter",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "getName", "parameterTypes" : [] }
+    ]
+  },
+  {
+    "name" : "java.nio.file.Path",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.nio.file.Paths",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "get", "parameterTypes" : ["java.lang.String", "[Ljava.lang.String;"] }
+    ]
+  },
+  {
+    "name" : "java.sql.Connection",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.sql.Driver",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "java.sql.DriverManager",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "getConnection", "parameterTypes" : ["java.lang.String"] },
+      { "name" : "getDriver", "parameterTypes" : ["java.lang.String"] }
+    ]
+  },
+  {
+    "name" : "java.sql.Timestamp",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "valueOf", "parameterTypes" : ["java.lang.String"] }
+    ]
+  },
+  {
+    "name" : "java.time.Duration",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.Instant",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.LocalDate",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.LocalDateTime",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.LocalTime",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.MonthDay",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.OffsetDateTime",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.OffsetTime",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.Period",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.Year",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.YearMonth",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.time.ZoneId",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "of", "parameterTypes" : ["java.lang.String"] }
+    ]
+  },
+  {
+    "name" : "java.time.ZoneOffset",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "of", "parameterTypes" : ["java.lang.String"] }
+    ]
+  },
+  {
+    "name" : "java.time.ZonedDateTime",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "parse", "parameterTypes" : ["java.lang.CharSequence"] }
+    ]
+  },
+  {
+    "name" : "java.util.ResourceBundle",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "methods" : [
+      { "name" : "getBaseBundleName", "parameterTypes" : [] }
+    ]
+  },
+  {
+    "name" : "org.fusesource.jansi.AnsiConsole",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "fields" : [
+      { "name" : "out" }
+    ]
+  },
+  {
+    "name" : "picocli.codegen.aot.graalvm.Issue622AbstractCommand",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "fields" : [
+      { "name" : "file" },
+      { "name" : "verbose" }
+    ]
+  },
+  {
+    "name" : "picocli.codegen.aot.graalvm.Issue622App",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "picocli.codegen.aot.graalvm.Issue622Command1",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "picocli.codegen.aot.graalvm.Issue622Command2",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "fields" : [
+      { "name" : "x" }
+    ]
+  },
+  {
+    "name" : "picocli.codegen.aot.graalvm.Issue622Command2Sub",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true,
+    "fields" : [
+      { "name" : "x" }
+    ]
+  }
+]


=====================================
picocli-examples/src/main/java/picocli/examples/interactive/PasswordDemo.java
=====================================
@@ -0,0 +1,46 @@
+package picocli.examples.interactive;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Model.CommandSpec;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.ParameterException;
+import picocli.CommandLine.Spec;
+
+import java.io.File;
+
+public class PasswordDemo implements Runnable {
+    @Option(names = "--password:file")
+    File passwordFile;
+
+    @Option(names = "--password:env")
+    String passwordEnvironmentVariable;
+
+    @Option(names = "--password", interactive = true)
+    String password;
+
+    @Spec
+    CommandSpec spec;
+
+    public void run() {
+        if (password != null) {
+            login(password);
+        } else if (passwordEnvironmentVariable != null) {
+            login(System.getenv(passwordEnvironmentVariable));
+        } else if (passwordFile != null) {
+            // below uses Java 8 NIO, create your own on older Java versions
+            /*
+            login(new String(Files.readAllBytes(passwordFile.toPath())));
+            */
+        } else {
+            throw new ParameterException(spec.commandLine(), "Password required");
+        }
+    }
+
+    private void login(String pwd) {
+        System.out.printf("Password: %s%n", pwd);
+    }
+
+    public static void main(String[] args) {
+        CommandLine.run(new PasswordDemo(), args);
+    }
+}


=====================================
src/main/java/picocli/CommandLine.java
=====================================
@@ -141,7 +141,7 @@ import static picocli.CommandLine.Help.Column.Overflow.WRAP;
 public class CommandLine {
     
     /** This is picocli version {@value}. */
-    public static final String VERSION = "3.9.2";
+    public static final String VERSION = "3.9.6";
 
     private final Tracer tracer = new Tracer();
     private final CommandSpec commandSpec;
@@ -749,7 +749,7 @@ public class CommandLine {
      * @since 0.9.7
      */
     public List<String> getUnmatchedArguments() {
-        return interpreter.parseResult == null ? Collections.<String>emptyList() : Collections.unmodifiableList(interpreter.parseResult.unmatched);
+        return interpreter.parseResult == null ? Collections.<String>emptyList() : UnmatchedArgumentException.stripErrorMessage(interpreter.parseResult.unmatched);
     }
 
     /**
@@ -1688,7 +1688,7 @@ public class CommandLine {
      */
     public void printVersionHelp(PrintStream out, Help.Ansi ansi, Object... params) {
         for (String versionInfo : getCommandSpec().version()) {
-            out.println(ansi.new Text(String.format(versionInfo, params)));
+            out.println(ansi.new Text(format(versionInfo, params)));
         }
     }
 
@@ -2387,6 +2387,16 @@ public class CommandLine {
     private static boolean isBoolean(Class<?> type) { return type == Boolean.class || type == Boolean.TYPE; }
     private static CommandLine toCommandLine(Object obj, IFactory factory) { return obj instanceof CommandLine ? (CommandLine) obj : new CommandLine(obj, factory);}
     private static boolean isMultiValue(Class<?> cls) { return cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls); }
+    private static String format(String formatString, Object... params) {
+        try {
+            return formatString == null ? "" : String.format(formatString, params);
+        } catch (IllegalFormatException ex) {
+            new Tracer().warn("Could not format '%s' (Underlying error: %s). " +
+                    "Using raw String: '%%n' format strings have not been replaced with newlines. " +
+                    "Please ensure to escape '%%' characters with another '%%'.%n", formatString, ex.getMessage());
+            return formatString;
+        }
+    }
 
     private static class NoCompletionCandidates implements Iterable<String> {
         public Iterator<String> iterator() { throw new UnsupportedOperationException(); }
@@ -2543,9 +2553,10 @@ public class CommandLine {
         boolean versionHelp() default false;
 
         /**
-         * Description of this option, used when generating the usage documentation.
-         * <p>
-         * From picocli 3.2, the usage string may contain variables that are rendered when help is requested.
+         * Description of this option, used when generating the usage documentation. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.
+         * </p><p>
+         * The description may contain variables that are rendered when help is requested.
          * The string {@code ${DEFAULT-VALUE}} is replaced with the default value of the option. This is regardless of
          * the command's {@link Command#showDefaultValues() showDefaultValues} setting or the option's {@link #showDefaultValue() showDefaultValue} setting.
          * The string {@code ${COMPLETION-CANDIDATES}} is replaced with the completion candidates generated by
@@ -2765,9 +2776,10 @@ public class CommandLine {
          */
         String index() default "";
 
-        /** Description of the parameter(s), used when generating the usage documentation.
-         * <p>
-         * From picocli 3.2, the usage string may contain variables that are rendered when help is requested.
+        /** Description of the parameter(s), used when generating the usage documentation. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.
+         * </p><p>
+         * The description may contain variables that are rendered when help is requested.
          * The string {@code ${DEFAULT-VALUE}} is replaced with the default value of the positional parameter. This is regardless of
          * the command's {@link Command#showDefaultValues() showDefaultValues} setting or the positional parameter's {@link #showDefaultValue() showDefaultValue} setting.
          * The string {@code ${COMPLETION-CANDIDATES}} is replaced with the completion candidates generated by
@@ -3122,7 +3134,9 @@ public class CommandLine {
         String separator() default "=";
 
         /** Version information for this command, to print to the console when the user specifies an
-         * {@linkplain Option#versionHelp() option} to request version help. This is not part of the usage help message.
+         * {@linkplain Option#versionHelp() option} to request version help. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
+         * <p>This is not part of the usage help message.</p>
          *
          * @return a string or an array of strings with version information about this command (each string in the array is displayed on a separate line).
          * @since 0.9.8
@@ -3158,21 +3172,22 @@ public class CommandLine {
          * @since 3.0 */
         boolean helpCommand() default false;
 
-        /** Set the heading preceding the header section. May contain embedded {@linkplain java.util.Formatter format specifiers}.
+        /** Set the heading preceding the header section.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the header section
          * @see UsageMessageSpec#headerHeading()
          * @see Help#headerHeading(Object...)  */
         String headerHeading() default "";
 
-        /** Optional summary description of the command, shown before the synopsis.
+        /** Optional summary description of the command, shown before the synopsis. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return summary description of the command
          * @see UsageMessageSpec#header()
          * @see Help#header(Object...)  */
         String[] header() default {};
 
-        /** Set the heading preceding the synopsis text. May contain embedded
-         * {@linkplain java.util.Formatter format specifiers}. The default heading is {@code "Usage: "} (without a line
-         * break between the heading and the synopsis text).
+        /** Set the heading preceding the synopsis text. The default heading is {@code "Usage: "} (without a line break between the heading and the synopsis text).
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the synopsis text
          * @see Help#synopsisHeading(Object...)  */
         String synopsisHeading() default "Usage: ";
@@ -3184,27 +3199,32 @@ public class CommandLine {
          * @see Help#detailedSynopsis(Comparator, boolean) */
         boolean abbreviateSynopsis() default false;
 
-        /** Specify one or more custom synopsis lines to display instead of an auto-generated synopsis.
+        /** Specify one or more custom synopsis lines to display instead of an auto-generated synopsis. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return custom synopsis text to replace the auto-generated synopsis
          * @see Help#customSynopsis(Object...) */
         String[] customSynopsis() default {};
 
-        /** Set the heading preceding the description section. May contain embedded {@linkplain java.util.Formatter format specifiers}.
+        /** Set the heading preceding the description section.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the description section
          * @see Help#descriptionHeading(Object...)  */
         String descriptionHeading() default "";
 
-        /** Optional text to display between the synopsis line(s) and the list of options.
+        /** Optional text to display between the synopsis line(s) and the list of options. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return description of this command
          * @see Help#description(Object...) */
         String[] description() default {};
 
-        /** Set the heading preceding the parameters list. May contain embedded {@linkplain java.util.Formatter format specifiers}.
+        /** Set the heading preceding the parameters list.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the parameters list
          * @see Help#parameterListHeading(Object...)  */
         String parameterListHeading() default "";
 
-        /** Set the heading preceding the options list. May contain embedded {@linkplain java.util.Formatter format specifiers}.
+        /** Set the heading preceding the options list.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the options list
          * @see Help#optionListHeading(Object...)  */
         String optionListHeading() default "";
@@ -3231,18 +3251,20 @@ public class CommandLine {
          * @return whether the default values for options and parameters should be shown in the description column */
         boolean showDefaultValues() default false;
 
-        /** Set the heading preceding the subcommands list. May contain embedded {@linkplain java.util.Formatter format specifiers}.
-         * The default heading is {@code "Commands:%n"} (with a line break at the end).
+        /** Set the heading preceding the subcommands list. The default heading is {@code "Commands:%n"} (with a line break at the end).
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the subcommands list
          * @see Help#commandListHeading(Object...)  */
         String commandListHeading() default "Commands:%n";
 
-        /** Set the heading preceding the footer section. May contain embedded {@linkplain java.util.Formatter format specifiers}.
+        /** Set the heading preceding the footer section.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return the heading preceding the footer section
          * @see Help#footerHeading(Object...)  */
         String footerHeading() default "";
 
-        /** Optional text to display after the list of options.
+        /** Optional text to display after the list of options. Each element of the array is rendered on a separate line.
+         * <p>May contain embedded {@linkplain java.util.Formatter format specifiers} like {@code %n} line separators. Literal percent {@code '%'} characters must be escaped with another {@code %}.</p>
          * @return text to display after the list of options
          * @see Help#footer(Object...) */
         String[] footer() default {};
@@ -3498,7 +3520,8 @@ public class CommandLine {
         static Range adjustForType(Range result, TypedMember member) {
             return result.isUnspecified ? defaultArity(member) : result;
         }
-        /** Returns the default arity {@code Range}: for {@link Option options} this is 0 for booleans and 1 for
+        /** Returns the default arity {@code Range}: for interactive options/positional parameters,
+         * this is 0; 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
@@ -3506,6 +3529,7 @@ public class CommandLine {
          * @since 2.0 */
         public static Range defaultArity(Field field) { return defaultArity(new TypedMember(field)); }
         private static Range defaultArity(TypedMember member) {
+            if (member.isInteractive()) { return Range.valueOf("0").unspecified(true); }
             Class<?> type = member.getType();
             if (member.isAnnotationPresent(Option.class)) {
                 Class<?>[] typeAttribute = ArgsReflection
@@ -3612,6 +3636,8 @@ public class CommandLine {
             int result = min - other.min;
             return (result == 0) ? max - other.max : result;
         }
+        /** Returns true for these ranges: 0 and 0..1. */
+        boolean isValidForInteractiveArgs() { return (min == 0 && (max == 0 || max == 1)); }
     }
     private static void validatePositionalParameters(List<PositionalParamSpec> positionalParametersFields) {
         int min = 0;
@@ -3853,12 +3879,15 @@ public class CommandLine {
              * @param subCommandLine the subcommand to envoke when the name is encountered on the command line
              * @return this {@code CommandSpec} object for method chaining */
             public CommandSpec addSubcommand(String name, CommandLine subCommandLine) {
+                Tracer t = new Tracer();
+                if (t.isDebug()) {t.debug("Adding subcommand '%s' to '%s'%n", name, this.qualifiedName());}
                 CommandLine previous = commands.put(name, subCommandLine);
                 if (previous != null && previous != subCommandLine) { throw new InitializationException("Another subcommand named '" + name + "' already exists for command '" + this.name() + "'"); }
                 CommandSpec subSpec = subCommandLine.getCommandSpec();
                 if (subSpec.name == null) { subSpec.name(name); }
                 subSpec.parent(this);
                 for (String alias : subSpec.aliases()) {
+                    if (t.isDebug()) {t.debug("Adding alias '%s' for subcommand '%s' to '%s'%n", alias, name, this.qualifiedName());}
                     previous = commands.put(alias, subCommandLine);
                     if (previous != null && previous != subCommandLine) { throw new InitializationException("Alias '" + alias + "' for subcommand '" + name + "' is already used by another subcommand of '" + this.name() + "'"); }
                 }
@@ -3892,15 +3921,21 @@ public class CommandLine {
              * @since 3.7.0
              */
             public CommandSpec addMethodSubcommands(IFactory factory) {
-                if (userObject() instanceof Method) {
-                     throw new InitializationException("Cannot discover subcommand methods of this Command Method: " + userObject());
+                if (userObject instanceof Method) {
+                    throw new InitializationException("Cannot discover subcommand methods of this Command Method: " + userObject());
                 }
-                for (Method method : getCommandMethods(userObject().getClass(), null)) {
-                    CommandLine cmd = new CommandLine(method, factory);
-                    addSubcommand(cmd.getCommandName(), cmd);
+                for (CommandLine sub : createMethodSubcommands(userObject().getClass(), factory)) {
+                    addSubcommand(sub.getCommandName(), sub);
                 }
                 return this;
             }
+            static List<CommandLine> createMethodSubcommands(Class<?> cls, IFactory factory) {
+                List<CommandLine> result = new ArrayList<CommandLine>();
+                for (Method method : getCommandMethods(cls, null)) {
+                    result.add(new CommandLine(method, factory));
+                }
+                return result;
+            }
 
             /** Returns the parent command of this subcommand, or {@code null} if this is a top-level command. */
             public CommandSpec parent() { return parent; }
@@ -4240,9 +4275,13 @@ public class CommandLine {
                     if (tracer != null && tracer.isDebug()) {tracer.debug("Parser is configured to treat all unmatched options as positional parameter%n", arg);}
                     return false;
                 }
+                if (arg.length() == 1) {
+                    if (tracer != null && tracer.isDebug()) {tracer.debug("Single-character arguments that don't match known options are considered positional parameters%n", arg);}
+                    return false;
+                }
                 if (options().isEmpty()) {
                     boolean result = arg.startsWith("-");
-                    if (tracer != null && tracer.isDebug()) {tracer.debug("%s %s an option%n", arg, (result ? "resembles" : "doesn't resemble"));}
+                    if (tracer != null && tracer.isDebug()) {tracer.debug("'%s' %s an option%n", arg, (result ? "resembles" : "doesn't resemble"));}
                     return result;
                 }
                 int count = 0;
@@ -4252,7 +4291,7 @@ public class CommandLine {
                     }
                 }
                 boolean result = count > 0 && count * 10 >= optionsMap().size() * 9; // at least one prefix char in common with 9 out of 10 options
-                if (tracer != null && 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, optionsMap().size());}
+                if (tracer != null && 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, optionsMap().size());}
                 return result;
             }
         }
@@ -5034,7 +5073,9 @@ public class CommandLine {
 
                 Range tempArity = builder.arity;
                 if (tempArity == null) {
-                    if (isOption()) {
+                    if (interactive) {
+                        tempArity = Range.valueOf("0");
+                    } else if (isOption()) {
                         tempArity = (builder.type == null || isBoolean(builder.type)) ? Range.valueOf("0") : Range.valueOf("1");
                     } else {
                         tempArity = Range.valueOf("1");
@@ -5045,7 +5086,9 @@ public class CommandLine {
     
                 if (builder.type == null) {
                     if (builder.auxiliaryTypes == null || builder.auxiliaryTypes.length == 0) {
-                        if (arity.isVariable || arity.max > 1) {
+                        if (interactive) {
+                            type = char[].class;
+                        } else if (arity.isVariable || arity.max > 1) {
                             type = String[].class;
                         } else if (arity.max == 1) {
                             type = String.class;
@@ -5060,9 +5103,13 @@ public class CommandLine {
                 }
                 if (builder.auxiliaryTypes == null || builder.auxiliaryTypes.length == 0) {
                     if (type.isArray()) {
-                        auxiliaryTypes = new Class<?>[]{type.getComponentType()};
+                        if (interactive && type.equals(char[].class)) {
+                            auxiliaryTypes = new Class<?>[]{char[].class};
+                        } else {
+                            auxiliaryTypes = new Class<?>[]{type.getComponentType()};
+                        }
                     } else if (Collection.class.isAssignableFrom(type)) { // type is a collection but element type is unspecified
-                        auxiliaryTypes = new Class<?>[] {String.class}; // use String elements
+                        auxiliaryTypes = new Class<?>[] {interactive ? char[].class : String.class}; // use String elements
                     } else if (Map.class.isAssignableFrom(type)) { // type is a map but element type is unspecified
                         auxiliaryTypes = new Class<?>[] {String.class, String.class}; // use String keys and String values
                     } else {
@@ -5078,8 +5125,8 @@ public class CommandLine {
                 } else {
                     completionCandidates = builder.completionCandidates;
                 }
-                if (interactive && (arity.min != 1 || arity.max != 1)) {
-                    throw new InitializationException("Interactive options and positional parameters are only supported for arity=1, not for arity=" + arity);
+                if (interactive && !arity.isValidForInteractiveArgs()) {
+                    throw new InitializationException("Interactive options and positional parameters are only supported for arity=0 and arity=0..1; not for arity=" + arity);
                 }
             }
 
@@ -5117,7 +5164,7 @@ public class CommandLine {
                 String defaultValueString = defaultValueString();
                 String[] result = new String[desc.length];
                 for (int i = 0; i < desc.length; i++) {
-                    result[i] = String.format(desc[i].replace(DESCRIPTION_VARIABLE_DEFAULT_VALUE, defaultValueString)
+                    result[i] = format(desc[i].replace(DESCRIPTION_VARIABLE_DEFAULT_VALUE, defaultValueString.replace("%", "%%"))
                             .replace(DESCRIPTION_VARIABLE_COMPLETION_CANDIDATES, candidates.toString()));
                 }
                 return result;
@@ -5187,14 +5234,7 @@ public class CommandLine {
              * @see CommandSpec#defaultValueProvider()
              * @see ArgSpec#defaultValue() */
             public String defaultValueString() {
-                String fromProvider = null;
-                IDefaultValueProvider defaultValueProvider = null;
-                try {
-                    defaultValueProvider = commandSpec.defaultValueProvider();
-                    fromProvider = defaultValueProvider == null ? null : defaultValueProvider.defaultValue(this);
-                } catch (Exception ex) {
-                    new Tracer().info("Error getting default value for %s from %s: %s", this, defaultValueProvider, ex);
-                }
+                String fromProvider = defaultValueFromProvider();
                 String defaultVal = fromProvider == null ? this.defaultValue() : fromProvider;
                 Object value = defaultVal == null ? initialValue() : defaultVal;
                 if (value != null && value.getClass().isArray()) {
@@ -5207,6 +5247,18 @@ public class CommandLine {
                 return String.valueOf(value);
             }
 
+            private String defaultValueFromProvider() {
+                String fromProvider = null;
+                IDefaultValueProvider defaultValueProvider = null;
+                try {
+                    defaultValueProvider = commandSpec.defaultValueProvider();
+                    fromProvider = defaultValueProvider == null ? null : defaultValueProvider.defaultValue(this);
+                } catch (Exception ex) {
+                    new Tracer().info("Error getting default value for %s from %s: %s", this, defaultValueProvider, ex);
+                }
+                return fromProvider;
+            }
+
             /** Returns the explicitly set completion candidates for this option or positional parameter, valid enum
              * constant names, or {@code null} if this option or positional parameter does not have any completion
              * candidates and its type is not an enum.
@@ -5276,7 +5328,7 @@ public class CommandLine {
             protected boolean internalShowDefaultValue(boolean usageHelpShowDefaults) {
                 if (showDefaultValue() == Help.Visibility.ALWAYS)   { return true; }  // override global usage help setting
                 if (showDefaultValue() == Help.Visibility.NEVER)    { return false; } // override global usage help setting
-                if (initialValue == null && defaultValue() == null) { return false; } // no default value to show
+                if (initialValue == null && defaultValue() == null && defaultValueFromProvider() == null) { return false; } // no default value to show
                 return usageHelpShowDefaults && !isBoolean(type());
             }
             /** Returns the Messages for this arg specification, or {@code null}.
@@ -6148,6 +6200,7 @@ public class CommandLine {
             boolean isUnmatched()    { return isAnnotationPresent(Unmatched.class); }
             boolean isInjectSpec()   { return isAnnotationPresent(Spec.class); }
             boolean isMultiValue()   { return CommandLine.isMultiValue(getType()); }
+            boolean isInteractive()  { return (isOption() && getAnnotation(Option.class).interactive()) || (isParameter() && getAnnotation(Parameters.class).interactive()); }
             IGetter getter()         { return getter; }
             ISetter setter()         { return setter; }
             Class<?> getType()       { return type; }
@@ -6371,13 +6424,13 @@ public class CommandLine {
                 if (!cls.isAnnotationPresent(Command.class)) { return false; }
 
                 Command cmd = cls.getAnnotation(Command.class);
-                return updateCommandAttributes(cmd, commandSpec, factory);
+                return updateCommandAttributes(cmd, cls, commandSpec, factory);
             }
             private static boolean updateCommandAttributes(Method method, CommandSpec commandSpec, IFactory factory) {
                 Command cmd = method.getAnnotation(Command.class);
-                return updateCommandAttributes(cmd, commandSpec, factory);
+                return updateCommandAttributes(cmd, null, commandSpec, factory);
             }
-            private static boolean updateCommandAttributes(Command cmd, CommandSpec commandSpec, IFactory factory) {
+            private static boolean updateCommandAttributes(Command cmd, Class<?> cls, CommandSpec commandSpec, IFactory factory) {
                 commandSpec.aliases(cmd.aliases());
                 commandSpec.parser().updateSeparator(cmd.separator());
                 commandSpec.updateName(cmd.name());
@@ -6387,10 +6440,10 @@ public class CommandLine {
                 commandSpec.initDefaultValueProvider(cmd.defaultValueProvider(), factory);
                 commandSpec.usageMessage().updateFromCommand(cmd, commandSpec);
 
-                initSubcommands(cmd, commandSpec, factory);
+                initSubcommands(cmd, cls, commandSpec, factory);
                 return true;
             }
-            private static void initSubcommands(Command cmd, CommandSpec parent, IFactory factory) {
+            private static void initSubcommands(Command cmd, Class<?> cls, CommandSpec parent, IFactory factory) {
                 for (Class<?> sub : cmd.subcommands()) {
                     try {
                         if (Help.class == sub) { throw new InitializationException(Help.class.getName() + " is not a valid subcommand. Did you mean " + HelpCommand.class.getName() + "?"); }
@@ -6406,8 +6459,10 @@ public class CommandLine {
                                 sub.getName() + ": " + ex, ex);
                     }
                 }
-                if (cmd.addMethodSubcommands() && !(parent.userObject() instanceof Method)) {
-                    parent.addMethodSubcommands(factory);
+                if (cmd.addMethodSubcommands() && cls != null) {
+                    for (CommandLine sub : CommandSpec.createMethodSubcommands(cls, factory)) {
+                        parent.addSubcommand(sub.getCommandName(), sub);
+                    }
                 }
             }
             static void initParentCommand(Object subcommand, Object parent) {
@@ -6685,12 +6740,17 @@ public class CommandLine {
                         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
+                            else 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; }
+                            } else if (paramTypes[i] instanceof GenericArrayType) {
+                                GenericArrayType gat = (GenericArrayType) paramTypes[i];
+                                if (char.class.equals(gat.getGenericComponentType())) {
+                                    result[i] = char[].class; continue;
+                                }
                             }
                             Arrays.fill(result, String.class); return result; // too convoluted generic type, giving up
                         }
@@ -6999,6 +7059,7 @@ public class CommandLine {
         private final Map<Class<?>, ITypeConverter<?>> converterRegistry = new HashMap<Class<?>, ITypeConverter<?>>();
         private boolean isHelpRequested;
         private int position;
+        private int interactiveCount;
         private boolean endOfOptions;
         private ParseResult.Builder parseResult;
 
@@ -7008,6 +7069,7 @@ public class CommandLine {
             converterRegistry.put(Object.class,        new BuiltIn.StringConverter());
             converterRegistry.put(String.class,        new BuiltIn.StringConverter());
             converterRegistry.put(StringBuilder.class, new BuiltIn.StringBuilderConverter());
+            converterRegistry.put(char[].class,        new BuiltIn.CharArrayConverter());
             converterRegistry.put(CharSequence.class,  new BuiltIn.CharSequenceConverter());
             converterRegistry.put(Byte.class,          new BuiltIn.ByteConverter());
             converterRegistry.put(Byte.TYPE,           new BuiltIn.ByteConverter());
@@ -7365,6 +7427,7 @@ public class CommandLine {
                 if (!endOfOptions && tracer.isDebug()) {tracer.debug("Parser was configured with stopAtPositional=true, treating remaining arguments as positional parameters.%n");}
                 endOfOptions = true;
             }
+            int originalInteractiveCount = this.interactiveCount;
             int argsConsumed = 0;
             int interactiveConsumed = 0;
             int originalNowProcessingSize = parseResult.nowProcessing.size();
@@ -7382,7 +7445,7 @@ public class CommandLine {
                 int count = originalSize - argsCopy.size();
                 if (count > 0 || actuallyConsumed > 0) {
                     required.remove(positionalParam);
-                    if (positionalParam.interactive()) { interactiveConsumed++; }
+                    interactiveConsumed = this.interactiveCount - originalInteractiveCount;
                 }
                 argsConsumed = Math.max(argsConsumed, count);
                 while (parseResult.nowProcessing.size() > originalNowProcessingSize + count) {
@@ -7477,6 +7540,9 @@ public class CommandLine {
                         }
                         // 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);}
+                        String tmp = args.pop();
+                        tmp = tmp + " (while processing option: '" + arg + "')";
+                        args.push(tmp);
                         handleUnmatchedArgument(args);
                     } else {
                         args.push(cluster);
@@ -7503,17 +7569,8 @@ public class CommandLine {
                 if (!assertNoMissingParameters(argSpec, arity, args)) { return 0; } // #389 collectErrors parsing
             }
 
-            if (argSpec.interactive()) {
-                String name = argSpec.isOption() ? ((OptionSpec) argSpec).longestName() : "position " + position;
-                String prompt = String.format("Enter value for %s (%s): ", name, str(argSpec.renderedDescription(), 0));
-                if (tracer.isDebug()) {tracer.debug("Reading value for %s from console...%n", name);}
-                char[] value = readPassword(prompt);
-                if (tracer.isDebug()) {tracer.debug("User entered '%s' for %s.%n", value, name);}
-                workingStack.push(new String(value));
-            }
-
             int result;
-            if (argSpec.type().isArray()) {
+            if (argSpec.type().isArray() && !(argSpec.interactive() && argSpec.type() == char[].class)) {
                 result = applyValuesToArrayField(argSpec, lookBehind, arity, workingStack, initialized, argDescription);
             } else if (Collection.class.isAssignableFrom(argSpec.type())) {
                 result = applyValuesToCollectionField(argSpec, lookBehind, arity, workingStack, initialized, argDescription);
@@ -7542,65 +7599,96 @@ public class CommandLine {
                 throw new MaxValuesExceededException(CommandLine.this, optionDescription("", argSpec, 0) +
                         " should be specified without '" + value + "' parameter");
             }
-            int result = arity.min; // the number or args we need to consume
+            int consumed = arity.min; // the number or args we need to consume
 
+            String actualValue = value;
+            char[] interactiveValue = null;
             Class<?> cls = argSpec.auxiliaryTypes()[0]; // field may be interface/abstract type, use annotation to get concrete type
             if (arity.min <= 0) { // value may be optional
+                boolean optionalValueExists = true; // assume we will use the command line value
+                consumed = 1;
 
                 // special logic for booleans: BooleanConverter accepts only "true" or "false".
                 if (cls == Boolean.class || cls == Boolean.TYPE) {
 
                     // boolean option with arity = 0..1 or 0..*: value MAY be a param
-                    if (arity.max > 0 && ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
-                        result = 1;            // if it is a varargs we only consume 1 argument if it is a boolean value
-                        if (!lookBehind.isAttached()) { parseResult.nowProcessing(argSpec, value); }
-                    } else if (lookBehind != LookBehind.ATTACHED_WITH_SEPARATOR) { // if attached, try converting the value to boolean (and fail if invalid value)
-                        // it's okay to ignore value if not attached to option
-                        if (value != null) {
-                            args.push(value); // we don't consume the value
-                        }
+                    boolean optionalWithBooleanValue = arity.max > 0 && ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value));
+                    if (!optionalWithBooleanValue && lookBehind != LookBehind.ATTACHED_WITH_SEPARATOR) { // if attached, try converting the value to boolean (and fail if invalid value)
+                        // don't process cmdline arg: it's okay to ignore value if not attached to option
                         if (commandSpec.parser().toggleBooleanFlags()) {
                             Boolean currentValue = (Boolean) argSpec.getValue();
-                            value = String.valueOf(currentValue == null || !currentValue); // #147 toggle existing boolean value
+                            actualValue = String.valueOf(currentValue == null || !currentValue); // #147 toggle existing boolean value
                         } else {
-                            value = "true";
+                            actualValue = "true";
                         }
+                        optionalValueExists = false;
+                        consumed = 0;
                     }
                 } else { // non-boolean option with optional value #325, #279
-                    if (isOption(value)) {
-                        args.push(value); // we don't consume the value
-                        value = "";
-                    } else if (value == null) {
-                        value = "";
-                    } else {
-                        if (!lookBehind.isAttached()) { parseResult.nowProcessing(argSpec, value); }
+                    if (isOption(value)) { // value is not a parameter
+                        actualValue = "";
+                        optionalValueExists = false;
+                        consumed = 0;
+                    } else if (value == null) { // stack is empty, option with arity=0..1 was the last arg
+                        actualValue = "";
+                        optionalValueExists = false;
+                        consumed = 0;
                     }
                 }
-            } else {
-                if (!lookBehind.isAttached()) { parseResult.nowProcessing(argSpec, value); }
+                // if argSpec is interactive, we may need to read the password from the console:
+                // - if arity = 0   : ALWAYS read from console
+                // - if arity = 0..1: ONLY read from console if user specified a non-option value
+                if (argSpec.interactive() && (arity.max == 0 || !optionalValueExists)) {
+                    interactiveValue = readPassword(argSpec);
+                    consumed = 0;
+                }
+            }
+            if (consumed == 0) { // optional value was not specified on command line, we made something up
+                if (value != null) {
+                    args.push(value); // we don't consume the command line value
+                }
+            } else { // value was non-optional or optional value was actually specified
+                // process the command line value
+                if (!lookBehind.isAttached()) { parseResult.nowProcessing(argSpec, value); } // update position for Completers
             }
-            if (noMoreValues && value == null) {
+            if (noMoreValues && actualValue == null && interactiveValue == null) {
                 return 0;
             }
-            ITypeConverter<?> converter = getTypeConverter(cls, argSpec, 0);
-            Object newValue = tryConvert(argSpec, -1, converter, value, cls);
+            Object newValue = interactiveValue;
+            String initValueMessage = "Setting %s to *** (masked interactive value) for %4$s%n";
+            String overwriteValueMessage = "Overwriting %s value with *** (masked interactive value) for %s%n";
+            if (!char[].class.equals(cls) && !char[].class.equals(argSpec.type())) {
+                if (interactiveValue != null) {
+                    actualValue = new String(interactiveValue);
+                }
+                ITypeConverter<?> converter = getTypeConverter(cls, argSpec, 0);
+                newValue = tryConvert(argSpec, -1, converter, actualValue, cls);
+                initValueMessage = "Setting %s to '%3$s' (was '%2$s') for %4$s%n";
+                overwriteValueMessage = "Overwriting %s value '%s' with '%s' for %s%n";
+            } else {
+                if (interactiveValue == null) { // setting command line arg to char[] field
+                    newValue = actualValue.toCharArray();
+                } else {
+                    actualValue = "***"; // mask interactive value
+                }
+            }
             Object oldValue = argSpec.getValue();
-            String traceMessage = "Setting %s to '%3$s' (was '%2$s') for %4$s%n";
+            String traceMessage = initValueMessage;
             if (initialized.contains(argSpec)) {
                 if (!isOverwrittenOptionsAllowed()) {
                     throw new OverwrittenOptionException(CommandLine.this, argSpec, optionDescription("", argSpec, 0) +  " should be specified only once");
                 }
-                traceMessage = "Overwriting %s value '%s' with '%s' for %s%n";
+                traceMessage = overwriteValueMessage;
             }
             initialized.add(argSpec);
 
             if (tracer.isInfo()) { tracer.info(traceMessage, argSpec.toString(), String.valueOf(oldValue), String.valueOf(newValue), argDescription); }
             argSpec.setValue(newValue);
-            parseResult.addOriginalStringValue(argSpec, value);// #279 track empty string value if no command line argument was consumed
-            parseResult.addStringValue(argSpec, value);
+            parseResult.addOriginalStringValue(argSpec, actualValue);// #279 track empty string value if no command line argument was consumed
+            parseResult.addStringValue(argSpec, actualValue);
             parseResult.addTypedValues(argSpec, position, newValue);
             parseResult.add(argSpec, position);
-            return result;
+            return 1;
         }
         private int applyValuesToMapField(ArgSpec argSpec,
                                           LookBehind lookBehind,
@@ -7777,7 +7865,7 @@ public class CommandLine {
             List<Object> converted = consumeArguments(argSpec, lookBehind, arity, args, type, argDescription);
             if (collection == null || (!collection.isEmpty() && !initialized.contains(argSpec))) {
                 tracer.debug("Initializing binding for %s with empty %s%n", optionDescription("", argSpec, 0), argSpec.type().getSimpleName());
-                collection = createCollection(argSpec.type()); // collection type
+                collection = createCollection(argSpec.type(), type); // collection type, element type
                 argSpec.setValue(collection);
             }
             initialized.add(argSpec);
@@ -7816,19 +7904,26 @@ public class CommandLine {
                 consumed = consumedCount(i + 1, initialSize, argSpec);
                 lookBehind = LookBehind.SEPARATE;
             }
+            if (argSpec.interactive() && argSpec.arity().max == 0) {
+                consumed = addPasswordToList(argSpec, type, result, consumed, argDescription);
+            }
             // now process the varargs if any
             for (int i = consumed; consumed < arity.max && !args.isEmpty(); i++) {
-                if (!varargCanConsumeNextValue(argSpec, args.peek())) { break; }
-
-                List<Object> typedValuesAtPosition = new ArrayList<Object>();
-                parseResult.addTypedValues(argSpec, currentPosition++, typedValuesAtPosition);
-                if (!canConsumeOneArgument(argSpec, arity, consumed, args.peek(), type, argDescription)) {
-                    break; // leave empty list at argSpec.typedValueAtPosition[currentPosition] so we won't try to consume that position again
+                if (argSpec.interactive() && argSpec.arity().max == 1 && !varargCanConsumeNextValue(argSpec, args.peek())) {
+                    // if interactive and arity = 0..1, we consume from command line if possible (if next arg not an option or subcommand)
+                    consumed = addPasswordToList(argSpec, type, result, consumed, argDescription);
+                } else {
+                    if (!varargCanConsumeNextValue(argSpec, args.peek())) { break; }
+                    List<Object> typedValuesAtPosition = new ArrayList<Object>();
+                    parseResult.addTypedValues(argSpec, currentPosition++, typedValuesAtPosition);
+                    if (!canConsumeOneArgument(argSpec, arity, consumed, args.peek(), type, argDescription)) {
+                        break; // leave empty list at argSpec.typedValueAtPosition[currentPosition] so we won't try to consume that position again
+                    }
+                    consumeOneArgument(argSpec, lookBehind, arity, consumed, args.pop(), type, typedValuesAtPosition, i, argDescription);
+                    result.addAll(typedValuesAtPosition);
+                    consumed = consumedCount(i + 1, initialSize, argSpec);
+                    lookBehind = LookBehind.SEPARATE;
                 }
-                consumeOneArgument(argSpec, lookBehind, arity, consumed, args.pop(), type, typedValuesAtPosition, i, argDescription);
-                result.addAll(typedValuesAtPosition);
-                consumed = consumedCount(i + 1, initialSize, argSpec);
-                lookBehind = LookBehind.SEPARATE;
             }
             if (result.isEmpty() && arity.min == 0 && arity.max <= 1 && isBoolean(type)) {
                 return Arrays.asList((Object) Boolean.TRUE);
@@ -7844,6 +7939,22 @@ public class CommandLine {
             return commandSpec.parser().splitFirst() ? (arg.stringValues().size() - initialSize) / 2 : i;
         }
 
+        private int addPasswordToList(ArgSpec argSpec, Class<?> type, List<Object> result, int consumed, String argDescription) {
+            char[] password = readPassword(argSpec);
+            if (tracer.isInfo()) {
+                tracer.info("Adding *** (masked interactive value) to %s for %s%n", argSpec.toString(), argDescription);
+            }
+            parseResult.addStringValue(argSpec, "***");
+            parseResult.addOriginalStringValue(argSpec, "***");
+            if (!char[].class.equals(argSpec.auxiliaryTypes()[0]) && !char[].class.equals(argSpec.type())) {
+                Object value = tryConvert(argSpec, consumed, getTypeConverter(type, argSpec, consumed), new String(password), type);
+                result.add(value);
+            } else {
+                result.add(password);
+            }
+            consumed++;
+            return consumed;
+        }
         private int consumeOneArgument(ArgSpec argSpec,
                                        LookBehind lookBehind,
                                        Range arity,
@@ -7868,6 +7979,7 @@ public class CommandLine {
             return ++index;
         }
         private boolean canConsumeOneArgument(ArgSpec argSpec, Range arity, int consumed, String arg, Class<?> type, String argDescription) {
+            if (char[].class.equals(argSpec.auxiliaryTypes()[0]) || char[].class.equals(argSpec.type())) { return true; }
             ITypeConverter<?> converter = getTypeConverter(type, argSpec, 0);
             try {
                 String[] values = argSpec.splitValue(trim(arg), commandSpec.parser(), arity, consumed);
@@ -7895,7 +8007,7 @@ public class CommandLine {
             return !isCommand && !isOption(nextValue);
         }
 
-        /**
+        /** Returns true if the specified arg is "--", a registered option, or potentially a clustered POSIX option.
          * Called when parsing varargs parameters for a multi-value option.
          * When an option is encountered, the remainder should not be interpreted as vararg elements.
          * @param arg the string to determine whether it is an option or not
@@ -7965,7 +8077,7 @@ public class CommandLine {
             return value;
         }
         @SuppressWarnings("unchecked")
-        private Collection<Object> createCollection(Class<?> collectionClass) throws Exception {
+        private Collection<Object> createCollection(Class<?> collectionClass, Class<?> elementType) throws Exception {
             if (collectionClass.isInterface()) {
                 if (List.class.isAssignableFrom(collectionClass)) {
                     return new ArrayList<Object>();
@@ -7978,8 +8090,12 @@ public class CommandLine {
                 }
                 return new ArrayList<Object>();
             }
+            if (EnumSet.class.isAssignableFrom(collectionClass) && Enum.class.isAssignableFrom(elementType)) {
+                Object enumSet = EnumSet.noneOf((Class<Enum>) elementType);
+                return (Collection<Object>) enumSet;
+            }
             // custom Collection implementation class must have default constructor
-            return (Collection<Object>) collectionClass.newInstance();
+            return (Collection<Object>) factory.create(collectionClass);
         }
         @SuppressWarnings("unchecked") private Map<Object, Object> createMap(Class<?> mapClass) throws Exception {
             try { // if it is an implementation class, instantiate it
@@ -7989,6 +8105,7 @@ public class CommandLine {
         }
         private ITypeConverter<?> getTypeConverter(final Class<?> type, ArgSpec argSpec, int index) {
             if (argSpec.converters().length > index) { return argSpec.converters()[index]; }
+            if (char[].class.equals(argSpec.type()) && argSpec.interactive()) { return converterRegistry.get(char[].class); }
             if (converterRegistry.containsKey(type)) { return converterRegistry.get(type); }
             if (type.isEnum()) {
                 return new ITypeConverter<Object>() {
@@ -8070,6 +8187,14 @@ public class CommandLine {
                         : value;
         }
 
+        char[] readPassword(ArgSpec argSpec) {
+            String name = argSpec.isOption() ? ((OptionSpec) argSpec).longestName() : "position " + position;
+            String prompt = String.format("Enter value for %s (%s): ", name, str(argSpec.renderedDescription(), 0));
+            if (tracer.isDebug()) {tracer.debug("Reading value for %s from console...%n", name);}
+            char[] result = readPassword(prompt);
+            if (tracer.isDebug()) {tracer.debug("User entered %d characters for %s.%n", result.length, name);}
+            return result;
+        }
         char[] readPassword(String prompt) {
             try {
                 Object console = System.class.getDeclaredMethod("console").invoke(null);
@@ -8084,6 +8209,8 @@ public class CommandLine {
                 } catch (IOException ex2) {
                     throw new IllegalStateException(ex2);
                 }
+            } finally {
+                interactiveCount++;
             }
         }
     }
@@ -8099,6 +8226,9 @@ public class CommandLine {
      * Inner class to group the built-in {@link ITypeConverter} implementations.
      */
     private static class BuiltIn {
+        static class CharArrayConverter implements ITypeConverter<char[]> {
+            public char[] convert(String value) { return value.toCharArray(); }
+        }
         static class StringConverter implements ITypeConverter<String> {
             public String convert(String value) { return value; }
         }
@@ -8914,9 +9044,6 @@ public class CommandLine {
             }
             return sb;
         }
-        private static String format(String formatString,  Object... params) {
-            return formatString == null ? "" : String.format(formatString, params);
-        }
         private int width() { return commandSpec.usageMessage().width(); }
         /** Returns command custom synopsis as a string. A custom synopsis can be zero or more lines, and can be
          * specified declaratively with the {@link Command#customSynopsis()} annotation attribute or programmatically
@@ -10033,11 +10160,11 @@ public class CommandLine {
             static boolean isPseudoTTY() { return isWindows() && (isXterm() || hasOsType()); }
 
             static boolean ansiPossible() {
-                if (forceDisabled())              { return false; }
-                if (forceEnabled())               { return true; }
-                if (isJansiConsoleInstalled())    { return true; }
-                if (hintDisabled())               { return false; }
-                if (!isTTY() && !isPseudoTTY())   { return false; }
+                if (forceDisabled())                          { return false; }
+                if (forceEnabled())                           { return true; }
+                if (isWindows() && isJansiConsoleInstalled()) { return true; } // #630 JVM crash loading jansi.AnsiConsole on Linux
+                if (hintDisabled())                           { return false; }
+                if (!isTTY() && !isPseudoTTY())               { return false; }
                 return hintEnabled() || !isWindows() || isXterm() || hasOsType();
             }
             static boolean isJansiConsoleInstalled() {
@@ -10702,12 +10829,12 @@ public class CommandLine {
      * {@link Option} or {@link Parameters}. */
     public static class UnmatchedArgumentException extends ParameterException {
         private static final long serialVersionUID = -8700426380701452440L;
-        private List<String> unmatched;
+        private List<String> unmatched = Collections.<String>emptyList();
         public UnmatchedArgumentException(CommandLine commandLine, String msg) { super(commandLine, msg); }
         public UnmatchedArgumentException(CommandLine commandLine, Stack<String> args) { this(commandLine, new ArrayList<String>(reverse(args))); }
         public UnmatchedArgumentException(CommandLine commandLine, List<String> args) {
             this(commandLine, describe(args, commandLine) + (args.size() == 1 ? ": " : "s: ") + str(args));
-            unmatched = args;
+            unmatched = args == null ? Collections.<String>emptyList() : args;
         }
         /** Returns {@code true} and prints suggested solutions to the specified stream if such solutions exist, otherwise returns {@code false}.
          * @since 3.3.0 */
@@ -10716,7 +10843,16 @@ public class CommandLine {
         }
         /** Returns the unmatched command line arguments.
          * @since 3.3.0 */
-        public List<String> getUnmatched() { return unmatched == null ? Collections.<String>emptyList() : Collections.unmodifiableList(unmatched); }
+        public List<String> getUnmatched() { return stripErrorMessage(unmatched); }
+        static List<String> stripErrorMessage(List<String> unmatched) {
+            List<String> result = new ArrayList<String>();
+            for (String s : unmatched) {
+                if (s == null) { result.add(null); }
+                int pos = s.indexOf(" (while processing option:");
+                result.add(pos < 0 ? s : s.substring(0, pos));
+            }
+            return Collections.unmodifiableList(result);
+        }
         /** Returns {@code true} if the first unmatched command line arguments resembles an option, {@code false} otherwise.
          * @since 3.3.0 */
         public boolean isUnknownOption() { return isUnknownOption(unmatched, getCommandLine()); }


=====================================
src/test/java/picocli/AutoCompleteTest.java
=====================================
@@ -557,7 +557,7 @@ public class AutoCompleteTest {
                 "# =======================\n" +
                 "#\n" +
                 "# Bash completion support for the `picocli.AutoComplete` command,\n" +
-                "# generated by [picocli](http://picocli.info/) version 3.9.2.\n" +
+                "# generated by [picocli](http://picocli.info/) version 3.9.6.\n" +
                 "#\n" +
                 "# Installation\n" +
                 "# ------------\n" +
@@ -723,7 +723,7 @@ public class AutoCompleteTest {
                 "# =======================\n" +
                 "#\n" +
                 "# Bash completion support for the `nondefault` command,\n" +
-                "# generated by [picocli](http://picocli.info/) version 3.9.2.\n" +
+                "# generated by [picocli](http://picocli.info/) version 3.9.6.\n" +
                 "#\n" +
                 "# Installation\n" +
                 "# ------------\n" +


=====================================
src/test/java/picocli/CommandLineArityTest.java
=====================================
@@ -639,7 +639,7 @@ public class CommandLineArityTest {
             CommandLine.populateCommand(new BooleanOptionsArity0_nAndParameters(), "-rv234 -bool".split(" "));
             fail("Expected exception");
         } catch (UnmatchedArgumentException ok) {
-            assertEquals("Unknown option: -234", ok.getMessage());
+            assertEquals("Unknown option: -234 (while processing option: '-rv234')", ok.getMessage());
         }
     }
     @Test


=====================================
src/test/java/picocli/CommandLineCommandMethodTest.java
=====================================
@@ -501,7 +501,7 @@ public class CommandLineCommandMethodTest {
             CommandLine.populateCommand(m, "-oout -r -vp1 p2".split(" "));
             fail("should fail: -v does not take an argument");
         } catch (UnmatchedArgumentException ex) {
-            assertEquals("Unknown option: -p1", ex.getMessage());
+            assertEquals("Unknown option: -p1 (while processing option: '-vp1')", ex.getMessage());
         }
     }
 


=====================================
src/test/java/picocli/CommandLineDefaultProviderTest.java
=====================================
@@ -209,7 +209,8 @@ public class CommandLineDefaultProviderTest {
                 "  -d=<string>    Default: XYZ%n");
         CommandLine cmd = new CommandLine(App.class);
         cmd.setDefaultValueProvider(new IDefaultValueProvider() {
-            public String defaultValue(ArgSpec argSpec) throws Exception {
+            public String defaultValue(ArgSpec argSpec) throws
+                    Exception {
                 return "XYZ";
             }
         });
@@ -243,4 +244,35 @@ public class CommandLineDefaultProviderTest {
         });
         assertEquals(expected2, cmd.getUsageMessage(CommandLine.Help.Ansi.OFF));
     }
+    static class FooDefaultProvider implements IDefaultValueProvider {
+        public String defaultValue(ArgSpec argSpec) throws Exception {
+            return "DURATION".equals(argSpec.paramLabel()) ? "1200" : null;
+        }
+    }
+
+    @Test
+    public void testIssue616DefaultProviderWithShowDefaultValues() {
+        @Command(name = "foo", mixinStandardHelpOptions = true,
+        defaultValueProvider = FooDefaultProvider.class,
+        showDefaultValues = true)
+        class FooCommand implements Runnable {
+
+            @Option(names = {"-d", "--duration"}, paramLabel = "DURATION", arity = "1",
+            description = "The duration, in seconds.")
+            Integer duration;
+
+            public void run() {
+                System.out.printf("duration=%s%n", duration);
+            }
+        }
+
+        String expected = String.format("" +
+                "Usage: foo [-hV] [-d=DURATION]%n" +
+                "  -d, --duration=DURATION   The duration, in seconds.%n" +
+                "                              Default: 1200%n" +
+                "  -h, --help                Show this help message and exit.%n" +
+                "  -V, --version             Print version information and exit.%n");
+        String actual = new CommandLine(new FooCommand()).getUsageMessage();
+        assertEquals(expected, actual);
+    }
 }


=====================================
src/test/java/picocli/CommandLineHelpTest.java
=====================================
@@ -19,6 +19,7 @@ import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.contrib.java.lang.system.ProvideSystemProperty;
+import org.junit.contrib.java.lang.system.SystemErrRule;
 import org.junit.contrib.java.lang.system.SystemOutRule;
 import picocli.CommandLine.*;
 import picocli.CommandLine.Model.*;
@@ -55,6 +56,8 @@ public class CommandLineHelpTest {
     public final ProvideSystemProperty ansiOFF = new ProvideSystemProperty("picocli.ansi", "false");
     @Rule
     public final SystemOutRule systemOutRule = new SystemOutRule().enableLog().muteForSuccessfulTests();
+    @Rule
+    public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests();
 
     @After
     public void after() {
@@ -2526,7 +2529,7 @@ public class CommandLineHelpTest {
 
     @Test
     public void testFormat() throws Exception {
-        Method m = Help.class.getDeclaredMethod("format", String.class, Object[].class);
+        Method m = CommandLine.class.getDeclaredMethod("format", String.class, Object[].class);
         m.setAccessible(true);
 
         String result = (String) m.invoke(null, (String) null, new Object[]{"abc"});
@@ -3712,4 +3715,55 @@ public class CommandLineHelpTest {
                 "  -V, --version             Print version information and exit.%n");
         assertEquals(expected, new CommandLine(new ExampleCommand()).getUsageMessage(Help.Ansi.OFF));
     }
+
+    @Test
+    public void testIssue615DescriptionContainingPercentChar() {
+        class App {
+            @Option(names = {"--excludebase"},
+                    arity="1..*",
+                    description = "exclude child files of cTree (only works with --ctree).%n"
+                            + "Currently must be explicit or with trailing % for truncated glob."
+            )
+            public String[] excludeBase;
+        }
+        String expected = String.format("" +
+                "Usage: <main class> [--excludebase=<excludeBase>...]...%n" +
+                "      --excludebase=<excludeBase>...%n" +
+                "         exclude child files of cTree (only works with --ctree).%%nCurrently must be%n" +
+                "           explicit or with trailing %% for truncated glob.%n");
+        String actual = new CommandLine(new App()).getUsageMessage();
+        assertEquals(expected, actual);
+
+        assertTrue(systemErrRule.getLog().contains(
+                "[picocli WARN] Could not format 'exclude child files of cTree (only works with --ctree).%n" +
+                        "Currently must be explicit or with trailing % for truncated glob.' " +
+                        "(Underlying error:"));
+        assertTrue(systemErrRule.getLog().contains(
+                "). " +
+                        "Using raw String: '%n' format strings have not been replaced with newlines. " +
+                        "Please ensure to escape '%' characters with another '%'."));
+    }
+
+    @Test
+    public void testDescriptionWithDefaultValueContainingPercentChar() {
+        class App {
+            @Option(names = {"-f"},
+                    defaultValue = "%s - page %d of %d",
+                    description = "format string. Default: ${DEFAULT-VALUE}")
+            public String formatString;
+        }
+        String expected = String.format("" +
+                "Usage: <main class> [-f=<formatString>]%n" +
+                "  -f=<formatString>    format string. Default: %%s - page %%d of %%d%n");
+        String actual = new CommandLine(new App()).getUsageMessage();
+        assertEquals(expected, actual);
+
+        assertFalse(systemErrRule.getLog().contains(
+                "[picocli WARN] Could not format 'format string. Default: %s - page %d of %d' " +
+                        "(Underlying error:"));
+        assertFalse(systemErrRule.getLog().contains(
+                "). " +
+                        "Using raw String: '%n' format strings have not been replaced with newlines. " +
+                        "Please ensure to escape '%' characters with another '%'."));
+    }
 }


=====================================
src/test/java/picocli/CommandLineMixinTest.java
=====================================
@@ -26,6 +26,7 @@ import java.io.UnsupportedEncodingException;
 import java.util.concurrent.Callable;
 
 import static org.junit.Assert.*;
+import static picocli.HelpTestUtil.setTraceLevel;
 import static picocli.HelpTestUtil.usageString;
 
 public class CommandLineMixinTest {
@@ -864,4 +865,33 @@ public class CommandLineMixinTest {
             assertEquals("RAPTOR error", ex.getMessage());
         }
     }
+
+    @Command(name="super")
+    static class SuperClass {
+    }
+
+    @Command(name="sub")
+    static class SubClass extends SuperClass {
+        @Command(name="method")
+        public void method() {}
+    }
+    @Command(name="main", subcommands= {SuperClass.class, SubClass.class})
+    static class Main {
+    }
+
+    @Test
+    public void testIssue619MethodSubcommandInSubclassAddedTwice() {
+        //setTraceLevel("DEBUG");
+        CommandLine commandLine = new CommandLine(new Main());
+        assertEquals(2, commandLine.getSubcommands().size());
+
+        CommandLine zuper = commandLine.getSubcommands().get("super");
+        assertEquals(0, zuper.getSubcommands().size());
+
+        CommandLine sub = commandLine.getSubcommands().get("sub");
+        assertEquals(1, sub.getSubcommands().size());
+
+        CommandLine method = sub.getSubcommands().get("method");
+        assertEquals(0, method.getSubcommands().size());
+    }
 }


=====================================
src/test/java/picocli/CommandLineTest.java
=====================================
@@ -146,7 +146,7 @@ public class CommandLineTest {
     }
     @Test
     public void testVersion() {
-        assertEquals("3.9.2", CommandLine.VERSION);
+        assertEquals("3.9.6", CommandLine.VERSION);
     }
     @Test
     public void testArrayPositionalParametersAreReplacedNotAppendedTo() {
@@ -589,7 +589,7 @@ 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("Unknown option: -p1", ex.getMessage());
+            assertEquals("Unknown option: -p1 (while processing option: '-vp1')", ex.getMessage());
         }
     }
 
@@ -1736,17 +1736,29 @@ public class CommandLineTest {
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$Git with factory picocli.CommandLine$DefaultFactory%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.CommandLine$AutoHelpMixin with factory picocli.CommandLine$DefaultFactory%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.CommandLine$HelpCommand with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'help' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitStatus with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'status' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitCommit with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'commit' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitAdd with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'add' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitBranch with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'branch' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitCheckout with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'checkout' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitClone with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'clone' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitDiff with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'diff' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitMerge with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'merge' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitPush with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'push' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitRebase with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'rebase' to 'git'%n" +
                         "[picocli DEBUG] Creating CommandSpec for object of class picocli.Demo$GitTag with factory picocli.CommandLine$DefaultFactory%n" +
+                        "[picocli DEBUG] Adding subcommand 'tag' to 'git'%n" +
                         "[picocli INFO] Picocli version: %3$s%n" +
                         "[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] Parser configuration: posixClusteredShortOptionsAllowed=true, stopAtPositional=false, stopAtUnmatched=false, separator=null, overwrittenOptionsAllowed=false, unmatchedArgumentsAllowed=false, expandAtFiles=true, atFileCommentChar=#, useSimplifiedAtFiles=false, endOfOptionsDelimiter=--, limitSplit=false, aritySatisfiedByAttachedOptionParam=false, toggleBooleanFlags=true, unmatchedOptionsArePositionalParams=false, collectErrors=false,caseInsensitiveEnumValuesAllowed=false, trimQuotes=false, splitQuotedStrings=false%n" +
@@ -3778,173 +3790,6 @@ public class CommandLineTest {
         assertFalse(actual, actual.contains("Possible solutions:"));
     }
 
-    @Test
-    public void testInteractiveOptionReadsFromStdIn() {
-        class App {
-            @Option(names = "-x", description = {"Pwd", "line2"}, interactive = true) int x;
-            @Option(names = "-z") int z;
-        }
-
-        PrintStream out = System.out;
-        InputStream in = System.in;
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            System.setOut(new PrintStream(baos));
-            System.setIn(new ByteArrayInputStream("123".getBytes()));
-
-            App app = new App();
-            CommandLine cmd = new CommandLine(app);
-            cmd.parse("-x");
-
-            assertEquals("Enter value for -x (Pwd): ", baos.toString());
-            assertEquals(123, app.x);
-            assertEquals(0, app.z);
-
-            cmd.parse("-z", "678");
-
-            assertEquals(0, app.x);
-            assertEquals(678, app.z);
-        } finally {
-            System.setOut(out);
-            System.setIn(in);
-        }
-    }
-
-    @Test
-    public void testInteractiveOptionReadsFromStdInMultiLinePrompt() {
-        class App {
-            @Option(names = "-x", description = {"Pwd%nline2", "ignored"}, interactive = true) int x;
-            @Option(names = "-z") int z;
-        }
-
-        PrintStream out = System.out;
-        InputStream in = System.in;
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            System.setOut(new PrintStream(baos));
-            System.setIn(new ByteArrayInputStream("123".getBytes()));
-
-            App app = new App();
-            CommandLine cmd = new CommandLine(app);
-            cmd.parse("-x", "-z", "987");
-
-            String expectedPrompt = format("Enter value for -x (Pwd%nline2): ");
-            assertEquals(expectedPrompt, baos.toString());
-            assertEquals(123, app.x);
-            assertEquals(987, app.z);
-        } finally {
-            System.setOut(out);
-            System.setIn(in);
-        }
-    }
-
-    @Test
-    public void testInteractivePositionalReadsFromStdIn() {
-        class App {
-            @Parameters(index = "0", description = {"Pwd%nline2", "ignored"}, interactive = true) int x;
-            @Parameters(index = "1") int z;
-        }
-
-        PrintStream out = System.out;
-        InputStream in = System.in;
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            System.setOut(new PrintStream(baos));
-            System.setIn(new ByteArrayInputStream("123".getBytes()));
-
-            App app = new App();
-            CommandLine cmd = new CommandLine(app);
-            cmd.parse("987");
-
-            String expectedPrompt = format("Enter value for position 0 (Pwd%nline2): ");
-            assertEquals(expectedPrompt, baos.toString());
-            assertEquals(123, app.x);
-            assertEquals(987, app.z);
-        } finally {
-            System.setOut(out);
-            System.setIn(in);
-        }
-    }
-
-    @Test
-    public void testInteractivePositional2ReadsFromStdIn() {
-        class App {
-            @Parameters(index = "0") int a;
-            @Parameters(index = "1", description = {"Pwd%nline2", "ignored"}, interactive = true) int x;
-            @Parameters(index = "2") int z;
-        }
-
-        PrintStream out = System.out;
-        InputStream in = System.in;
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            System.setOut(new PrintStream(baos));
-            System.setIn(new ByteArrayInputStream("123".getBytes()));
-
-            App app = new App();
-            CommandLine cmd = new CommandLine(app);
-            cmd.parse("333", "987");
-
-            String expectedPrompt = format("Enter value for position 1 (Pwd%nline2): ");
-            assertEquals(expectedPrompt, baos.toString());
-            assertEquals(333, app.a);
-            assertEquals(123, app.x);
-            assertEquals(987, app.z);
-        } finally {
-            System.setOut(out);
-            System.setIn(in);
-        }
-    }
-
-    @Test
-    public void testLoginExample() {
-        class Login implements Callable<Object> {
-            @Option(names = {"-u", "--user"}, description = "User name")
-            String user;
-
-            @Option(names = {"-p", "--password"}, description = "Password or passphrase", interactive = true)
-            String password;
-
-            public Object call() throws Exception {
-                MessageDigest md = MessageDigest.getInstance("SHA-256");
-                md.update(password.getBytes());
-                System.out.printf("Hi %s, your password is hashed to %s.%n", user, base64(md.digest()));
-                return null;
-            }
-
-            private String base64(byte[] arr) throws Exception {
-                //return javax.xml.bind.DatatypeConverter.printBase64Binary(arr);
-                try {
-                    Object enc = Class.forName("java.util.Base64").getDeclaredMethod("getEncoder").invoke(null, new Object[0]);
-                    return (String) Class.forName("java.util.Base64$Encoder").getDeclaredMethod("encodeToString", new Class[]{byte[].class}).invoke(enc, new Object[] {arr});
-                } catch (Exception beforeJava8) {
-                    //return new sun.misc.BASE64Encoder().encode(arr);
-                    return "75K3eLr+dx6JJFuJ7LwIpEpOFmwGZZkRiB84PURz6U8="; // :-)
-                }
-            }
-        }
-
-        PrintStream out = System.out;
-        InputStream in = System.in;
-        try {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            System.setOut(new PrintStream(baos));
-            System.setIn(new ByteArrayInputStream("password123".getBytes()));
-
-            Login login = new Login();
-            CommandLine.call(login, "-u", "user123", "-p");
-
-            String expectedPrompt = format("Enter value for --password (Password or passphrase): " +
-                    "Hi user123, your password is hashed to 75K3eLr+dx6JJFuJ7LwIpEpOFmwGZZkRiB84PURz6U8=.%n");
-            assertEquals(expectedPrompt, baos.toString());
-            assertEquals("user123", login.user);
-            assertEquals("password123", login.password);
-        } finally {
-            System.setOut(out);
-            System.setIn(in);
-        }
-    }
-
     @Test
     public void testEmptyObjectArray() throws Exception {
         Method m = CommandLine.class.getDeclaredMethod("empty", new Class[] {Object[].class});
@@ -4165,4 +4010,24 @@ public class CommandLineTest {
         CommandLine commandLine = new CommandLine(new App());
         commandLine.parseArgs(new String[0]);
     }
+
+    @Test
+    public void testIssue613SingleDashPositionalParam() {
+        @Command(name = "dashtest", mixinStandardHelpOptions = true)
+        class App {
+            @Parameters(index = "0")
+            private String json;
+            @Parameters(index = "1")
+            private String template;
+        }
+        System.setProperty("picocli.trace", "DEBUG");
+        App app = new App();
+        CommandLine commandLine = new CommandLine(app);
+        //commandLine.setUnmatchedOptionsArePositionalParams(true);
+
+        commandLine.parseArgs("-", "~/hello.mustache");
+        assertEquals("-", app.json);
+        assertEquals("~/hello.mustache", app.template);
+        assertTrue(systemErrRule.getLog().contains("Single-character arguments that don't match known options are considered positional parameters"));
+    }
 }


=====================================
src/test/java/picocli/CommandLineTypeConversionTest.java
=====================================
@@ -47,6 +47,8 @@ import picocli.CommandLine.*;
 
 import static java.util.concurrent.TimeUnit.*;
 import static org.junit.Assert.*;
+import static picocli.CommandLineTypeConversionTest.ResultTypes.COMPLETE;
+import static picocli.CommandLineTypeConversionTest.ResultTypes.PARTIAL;
 
 public class CommandLineTypeConversionTest {
     // allows tests to set any kind of properties they like, without having to individually roll them back
@@ -1087,4 +1089,42 @@ public class CommandLineTypeConversionTest {
             assertEquals("Unmatched arguments: a:c, 1:3", ex.getMessage());
         }
     }
+    enum ResultTypes {
+        NONE,
+        PARTIAL,
+        COMPLETE
+    }
+    @Test
+    public void testIssue628EnumSetWithNullInitialValue() {
+        class App {
+            @Option(names = "--result-types", split = ",")
+            private EnumSet<ResultTypes> resultTypes = null;
+        }
+        App app = new App();
+        new CommandLine(app).parseArgs("--result-types", "PARTIAL,COMPLETE");
+
+        assertEquals(EnumSet.of(PARTIAL, COMPLETE), app.resultTypes);
+    }
+    @Test
+    public void testIssue628EnumSetWithEmptyInitialValue() {
+        class App {
+            @Option(names = "--result-types", split = ",")
+            private EnumSet<ResultTypes> resultTypes = EnumSet.noneOf(ResultTypes.class);
+        }
+        App app = new App();
+        new CommandLine(app).parseArgs("--result-types", "PARTIAL,COMPLETE");
+
+        assertEquals(EnumSet.of(PARTIAL, COMPLETE), app.resultTypes);
+    }
+    @Test
+    public void testIssue628EnumSetWithNonEmptyInitialValue() {
+        class App {
+            @Option(names = "--result-types", split = ",")
+            private EnumSet<ResultTypes> resultTypes = EnumSet.of(ResultTypes.COMPLETE);
+        }
+        App app = new App();
+        new CommandLine(app).parseArgs("--result-types", "PARTIAL,COMPLETE");
+
+        assertEquals(EnumSet.of(PARTIAL, COMPLETE), app.resultTypes);
+    }
 }


=====================================
src/test/java/picocli/InteractiveArgTest.java
=====================================
@@ -0,0 +1,665 @@
+package picocli;
+
+import org.junit.Test;
+import picocli.CommandLine.Option;
+import picocli.CommandLine.Parameters;
+import picocli.CommandLine.UnmatchedArgumentException;
+
+import java.io.*;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import static java.lang.String.format;
+import static org.junit.Assert.*;
+
+public class InteractiveArgTest {
+
+    @Test
+    public void testInteractiveOptionReadsFromStdIn() {
+        class App {
+            @Option(names = "-x", description = {"Pwd", "line2"}, interactive = true) int x;
+            @Option(names = "-z") int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(new ByteArrayInputStream("123".getBytes()));
+
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x");
+
+            assertEquals("Enter value for -x (Pwd): ", baos.toString());
+            assertEquals(123, app.x);
+            assertEquals(0, app.z);
+
+            cmd.parse("-z", "678");
+
+            assertEquals(0, app.x);
+            assertEquals(678, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionAsListOfIntegers() throws IOException {
+        class App {
+            @Option(names = "-x", description = {"Pwd", "line2"}, interactive = true)
+            List<Integer> x;
+
+            @Option(names = "-z")
+            int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-x");
+
+            assertEquals("Enter value for -x (Pwd): Enter value for -x (Pwd): ", baos.toString());
+            assertEquals(Arrays.asList(123, 123), app.x);
+            assertEquals(0, app.z);
+
+            cmd.parse("-z", "678");
+
+            assertNull(app.x);
+            assertEquals(678, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    ByteArrayInputStream inputStream(final String value) {
+        return new ByteArrayInputStream(value.getBytes()) {
+            int count;
+
+            @Override
+            public synchronized int read(byte[] b, int off, int len) {
+                System.arraycopy(value.getBytes(), 0, b, off, value.length());
+                return (count++ % 3) == 0 ? value.length() : -1;
+            }
+        };
+    }
+
+    @Test
+    public void testInteractiveOptionAsListOfCharArrays() throws IOException {
+        class App {
+            @Option(names = "-x", description = {"Pwd", "line2"}, interactive = true)
+            List<char[]> x;
+
+            @Option(names = "-z")
+            int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-x");
+
+            assertEquals("Enter value for -x (Pwd): Enter value for -x (Pwd): ", baos.toString());
+            assertEquals(2, app.x.size());
+            assertArrayEquals("123".toCharArray(), app.x.get(0));
+            assertArrayEquals("123".toCharArray(), app.x.get(1));
+            assertEquals(0, app.z);
+
+            cmd.parse("-z", "678");
+
+            assertNull(app.x);
+            assertEquals(678, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionAsCharArray() throws IOException {
+        class App {
+            @Option(names = "-x", description = {"Pwd", "line2"}, interactive = true)
+            char[] x;
+
+            @Option(names = "-z")
+            int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x");
+
+            assertEquals("Enter value for -x (Pwd): ", baos.toString());
+            assertArrayEquals("123".toCharArray(), app.x);
+            assertEquals(0, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionArity_0_1_ConsumesFromCommandLineIfPossible() throws IOException {
+        class App {
+            @Option(names = "-x", arity = "0..1", interactive = true)
+            char[] x;
+
+            @Parameters()
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "456", "abc");
+
+            assertArrayEquals("456".toCharArray(), app.x);
+            assertArrayEquals(new String[]{"abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionAsListOfCharArraysArity_0_1_ConsumesFromCommandLineIfPossible() throws IOException {
+        class App {
+            @Option(names = "-x", arity = "0..1", interactive = true)
+            List<char[]> x;
+
+            @Parameters()
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "456", "-x", "789", "abc");
+
+            assertEquals(2, app.x.size());
+            assertArrayEquals("456".toCharArray(), app.x.get(0));
+            assertArrayEquals("789".toCharArray(), app.x.get(1));
+            assertArrayEquals(new String[]{"abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionArity_0_1_AvoidsConsumingOption() throws IOException {
+        class App {
+            @Option(names = "-x", arity = "0..1", interactive = true)
+            char[] x;
+
+            @Option(names = "-z")
+            int z;
+
+            @Parameters()
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-z", "456", "abc");
+
+            assertArrayEquals("123".toCharArray(), app.x);
+            assertEquals(456, app.z);
+            assertArrayEquals(new String[]{"abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionAsListOfCharArraysArity_0_1_AvoidsConsumingOption() throws IOException {
+        class App {
+            @Option(names = "-x", arity = "0..1", interactive = true)
+            List<char[]> x;
+
+            @Option(names = "-z")
+            int z;
+
+            @Parameters()
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-z", "456", "abc");
+
+            assertEquals(1, app.x.size());
+            assertArrayEquals("123".toCharArray(), app.x.get(0));
+            assertEquals(456, app.z);
+            assertArrayEquals(new String[]{"abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionArity_0_1_ConsumesUnknownOption() throws IOException {
+        class App {
+            @Option(names = "-x", arity = "0..1", interactive = true)
+            char[] x;
+
+            @Option(names = "-z")
+            int z;
+
+            @Parameters()
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-y", "456", "abc");
+
+            assertArrayEquals("-y".toCharArray(), app.x);
+            assertEquals(0, app.z);
+            assertArrayEquals(new String[]{"456", "abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionAsListOfCharArraysArity_0_1_ConsumesUnknownOption() throws IOException {
+        class App {
+            @Option(names = "-x", arity = "0..1", interactive = true)
+            List<char[]> x;
+
+            @Option(names = "-z")
+            int z;
+
+            @Parameters()
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-y", "-x", "-w", "456", "abc");
+
+            assertEquals(2, app.x.size());
+            assertArrayEquals("-y".toCharArray(), app.x.get(0));
+            assertArrayEquals("-w".toCharArray(), app.x.get(1));
+            assertEquals(0, app.z);
+            assertArrayEquals(new String[]{"456", "abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractiveOptionReadsFromStdInMultiLinePrompt() {
+        class App {
+            @Option(names = "-x", description = {"Pwd%nline2", "ignored"}, interactive = true) int x;
+            @Option(names = "-z") int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(new ByteArrayInputStream("123".getBytes()));
+
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("-x", "-z", "987");
+
+            String expectedPrompt = format("Enter value for -x (Pwd%nline2): ");
+            assertEquals(expectedPrompt, baos.toString());
+            assertEquals(123, app.x);
+            assertEquals(987, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalReadsFromStdIn() {
+        class App {
+            @Parameters(index = "0", description = {"Pwd%nline2", "ignored"}, interactive = true) int x;
+            @Parameters(index = "1") int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(new ByteArrayInputStream("123".getBytes()));
+
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("987");
+
+            String expectedPrompt = format("Enter value for position 0 (Pwd%nline2): ");
+            assertEquals(expectedPrompt, baos.toString());
+            assertEquals(123, app.x);
+            assertEquals(987, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositional2ReadsFromStdIn() {
+        class App {
+            @Parameters(index = "0") int a;
+            @Parameters(index = "1", description = {"Pwd%nline2", "ignored"}, interactive = true) int x;
+            @Parameters(index = "2") int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(new ByteArrayInputStream("123".getBytes()));
+
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("333", "987");
+
+            String expectedPrompt = format("Enter value for position 1 (Pwd%nline2): ");
+            assertEquals(expectedPrompt, baos.toString());
+            assertEquals(333, app.a);
+            assertEquals(123, app.x);
+            assertEquals(987, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testLoginExample() {
+        class Login implements Callable<Object> {
+            @Option(names = {"-u", "--user"}, description = "User name")
+            String user;
+
+            @Option(names = {"-p", "--password"}, description = "Password or passphrase", interactive = true)
+            char[] password;
+
+            public Object call() throws Exception {
+                byte[] bytes = new byte[password.length];
+                for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) password[i]; }
+
+                MessageDigest md = MessageDigest.getInstance("SHA-256");
+                md.update(bytes);
+
+                System.out.printf("Hi %s, your password is hashed to %s.%n", user, base64(md.digest()));
+
+                // null out the arrays when done
+                Arrays.fill(bytes, (byte) 0);
+                Arrays.fill(password, ' ');
+
+                return null;
+            }
+
+            private String base64(byte[] arr) throws Exception {
+                //return javax.xml.bind.DatatypeConverter.printBase64Binary(arr);
+                try {
+                    Object enc = Class.forName("java.util.Base64").getDeclaredMethod("getEncoder").invoke(null, new Object[0]);
+                    return (String) Class.forName("java.util.Base64$Encoder").getDeclaredMethod("encodeToString", new Class[]{byte[].class}).invoke(enc, new Object[] {arr});
+                } catch (Exception beforeJava8) {
+                    //return new sun.misc.BASE64Encoder().encode(arr);
+                    return "75K3eLr+dx6JJFuJ7LwIpEpOFmwGZZkRiB84PURz6U8="; // :-)
+                }
+            }
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(inputStream("password123"));
+
+            Login login = new Login();
+            CommandLine.call(login, "-u", "user123", "-p");
+
+            String expectedPrompt = format("Enter value for --password (Password or passphrase): " +
+                    "Hi user123, your password is hashed to 75K3eLr+dx6JJFuJ7LwIpEpOFmwGZZkRiB84PURz6U8=.%n");
+            assertEquals(expectedPrompt, baos.toString());
+            assertEquals("user123", login.user);
+            assertArrayEquals("           ".toCharArray(), login.password);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalAsListOfCharArrays() throws IOException {
+        class App {
+            @Parameters(index = "0..1", description = {"Pwd", "line2"}, interactive = true)
+            List<char[]> x;
+            @Parameters(index = "2") int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("999");
+
+            assertEquals("Enter value for position 0 (Pwd): Enter value for position 1 (Pwd): ", baos.toString());
+            assertEquals(2, app.x.size());
+            assertArrayEquals("123".toCharArray(), app.x.get(0));
+            assertArrayEquals("123".toCharArray(), app.x.get(1));
+            assertEquals(999, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalAsCharArray() throws IOException {
+        class App {
+            @Parameters(index = "0", description = {"Pwd", "line2"}, interactive = true)
+            char[] x;
+            @Parameters(index = "1") int z;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            System.setOut(new PrintStream(baos));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("9");
+
+            assertEquals("Enter value for position 0 (Pwd): ", baos.toString());
+            assertArrayEquals("123".toCharArray(), app.x);
+            assertEquals(9, app.z);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalArity_0_1_ConsumesFromCommandLineIfPossible() throws IOException {
+        class App {
+            @Parameters(index = "0", arity = "0..1", interactive = true)
+            char[] x;
+
+            @Parameters(index = "1")
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("456", "abc");
+
+            assertArrayEquals("456".toCharArray(), app.x);
+            assertArrayEquals(new String[]{"abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalAsListOfCharArraysArity_0_1_ConsumesFromCommandLineIfPossible() throws IOException {
+        class App {
+            @Parameters(index = "0..1", arity = "0..1", interactive = true)
+            List<char[]> x;
+
+            @Parameters(index = "2")
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            cmd.parse("456", "789", "abc");
+
+            assertEquals(2, app.x.size());
+            assertArrayEquals("456".toCharArray(), app.x.get(0));
+            assertArrayEquals("789".toCharArray(), app.x.get(1));
+            assertArrayEquals(new String[]{"abc"}, app.remainder);
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalArity_0_1_DoesNotConsumeUnknownOption() throws IOException {
+        class App {
+            @Parameters(index = "0", arity = "0..1", interactive = true)
+            char[] x;
+
+            @Option(names = "-z")
+            int z;
+
+            @Parameters(index = "1")
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            try {
+                cmd.parse("-y", "456", "abc");
+                fail("Expect exception");
+            } catch (UnmatchedArgumentException ex) {
+                assertEquals("Unknown option: -y", ex.getMessage());
+            }
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+    @Test
+    public void testInteractivePositionalAsListOfCharArraysArity_0_1_DoesNotConsumeUnknownOption() throws IOException {
+        class App {
+            @Parameters(index = "0..1", arity = "0..1", interactive = true)
+            List<char[]> x;
+
+            @Option(names = "-z")
+            int z;
+
+            @Parameters(index = "2")
+            String[] remainder;
+        }
+
+        PrintStream out = System.out;
+        InputStream in = System.in;
+        try {
+            System.setOut(new PrintStream(new ByteArrayOutputStream()));
+            System.setIn(inputStream("123"));
+            App app = new App();
+            CommandLine cmd = new CommandLine(app);
+            try {
+                cmd.parse("-y", "-w", "456", "abc");
+                fail("Expect exception");
+            } catch (UnmatchedArgumentException ex) {
+                assertEquals("Unknown options: -y, -w", ex.getMessage());
+            }
+        } finally {
+            System.setOut(out);
+            System.setIn(in);
+        }
+    }
+
+}


=====================================
src/test/java/picocli/LenientParsingTest.java
=====================================
@@ -195,7 +195,7 @@ public class LenientParsingTest {
         cmd.getCommandSpec().parser().collectErrors(true);
         cmd.parse("-rv234 -bool".split(" "));
         assertEquals(1, cmd.getParseResult().errors().size());
-        assertEquals("Unknown option: -234", cmd.getParseResult().errors().get(0).getMessage());
+        assertEquals("Unknown option: -234 (while processing option: '-rv234')", cmd.getParseResult().errors().get(0).getMessage());
     }
 
     @Test


=====================================
src/test/java/picocli/ModelOptionSpecTest.java
=====================================
@@ -274,7 +274,7 @@ public class ModelOptionSpecTest {
     @Test
     public void testOptionInteractiveIfSet() {
         assertTrue(OptionSpec.builder("-x").interactive(true).interactive());
-        assertTrue(OptionSpec.builder("-x").arity("1").interactive(true).build().interactive());
+        assertTrue(OptionSpec.builder("-x").arity("0").interactive(true).build().interactive());
     }
 
     @Test
@@ -294,8 +294,7 @@ public class ModelOptionSpecTest {
     @Test
     public void testOptionInteractiveNotSupportedForMultiValue() {
         OptionSpec.Builder[] options = new OptionSpec.Builder[]{
-                OptionSpec.builder("-x").arity("0").interactive(true),
-                OptionSpec.builder("-x").arity("0..1").interactive(true),
+                OptionSpec.builder("-x").arity("1").interactive(true),
                 OptionSpec.builder("-x").arity("2").interactive(true),
                 OptionSpec.builder("-x").arity("3").interactive(true),
                 OptionSpec.builder("-x").arity("1..2").interactive(true),
@@ -307,9 +306,13 @@ public class ModelOptionSpecTest {
                 opt.build();
                 fail("Expected exception");
             } catch (InitializationException ex) {
-                assertEquals("Interactive options and positional parameters are only supported for arity=1, not for arity=" + opt.arity(), ex.getMessage());
+                assertEquals("Interactive options and positional parameters are only supported for arity=0 and arity=0..1; not for arity=" + opt.arity(), ex.getMessage());
             }
         }
+
+        // no errors
+        OptionSpec.builder("-x").arity("0").interactive(true).build();
+        OptionSpec.builder("-x").arity("0..1").interactive(true).build();
     }
 
     @Test


=====================================
src/test/java/picocli/ModelPositionalParamSpecTest.java
=====================================
@@ -198,8 +198,7 @@ public class ModelPositionalParamSpecTest {
     @Test
     public void testPositionalInteractiveNotSupportedForMultiValue() {
         PositionalParamSpec.Builder[] options = new PositionalParamSpec.Builder[]{
-                PositionalParamSpec.builder().arity("0").interactive(true),
-                PositionalParamSpec.builder().arity("0..1").interactive(true),
+                PositionalParamSpec.builder().arity("1").interactive(true),
                 PositionalParamSpec.builder().arity("2").interactive(true),
                 PositionalParamSpec.builder().arity("3").interactive(true),
                 PositionalParamSpec.builder().arity("1..2").interactive(true),
@@ -211,9 +210,12 @@ public class ModelPositionalParamSpecTest {
                 opt.build();
                 fail("Expected exception");
             } catch (CommandLine.InitializationException ex) {
-                assertEquals("Interactive options and positional parameters are only supported for arity=1, not for arity=" + opt.arity(), ex.getMessage());
+                assertEquals("Interactive options and positional parameters are only supported for arity=0 and arity=0..1; not for arity=" + opt.arity(), ex.getMessage());
             }
         }
+        // no errors
+        PositionalParamSpec.builder().arity("0").interactive(true).build();
+        PositionalParamSpec.builder().arity("0..1").interactive(true).build();
     }
 
     @Test



View it on GitLab: https://salsa.debian.org/java-team/picocli/commit/288475408bef888b63033a62a2346d8a06e982b8

-- 
View it on GitLab: https://salsa.debian.org/java-team/picocli/commit/288475408bef888b63033a62a2346d8a06e982b8
You're receiving this email because of your account on salsa.debian.org.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20190425/d1109024/attachment.html>


More information about the pkg-java-commits mailing list