add annotation support to CliBuilder

Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/09f137b0
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/09f137b0
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/09f137b0

Branch: refs/heads/master
Commit: 09f137b0dfffb42bcf5a976e5a991d3133c97341
Parents: 73e75fa
Author: paulk <pa...@asert.com.au>
Authored: Wed Apr 6 22:31:35 2016 +1000
Committer: paulk <pa...@asert.com.au>
Committed: Wed Apr 13 20:38:46 2016 +1000

----------------------------------------------------------------------
 src/main/groovy/cli/CliBuilderException.groovy  |  24 ++
 src/main/groovy/cli/EnhancedCommandLine.groovy  |  52 ----
 src/main/groovy/cli/Option.java                 |  49 +++-
 src/main/groovy/util/CliBuilder.groovy          | 283 +++++++++++++++----
 .../doc/core-domain-specific-languages.adoc     | 234 ++++++++++++++-
 src/spec/test/builder/CliBuilderTest.groovy     | 247 ++++++++++++++++
 src/test/groovy/util/CliBuilderTest.groovy      | 153 +++++++++-
 7 files changed, 918 insertions(+), 124 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/main/groovy/cli/CliBuilderException.groovy
----------------------------------------------------------------------
diff --git a/src/main/groovy/cli/CliBuilderException.groovy 
b/src/main/groovy/cli/CliBuilderException.groovy
new file mode 100644
index 0000000..84a9438
--- /dev/null
+++ b/src/main/groovy/cli/CliBuilderException.groovy
@@ -0,0 +1,24 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.cli
+
+import groovy.transform.InheritConstructors
+
+@InheritConstructors
+class CliBuilderException extends RuntimeException { }

http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/main/groovy/cli/EnhancedCommandLine.groovy
----------------------------------------------------------------------
diff --git a/src/main/groovy/cli/EnhancedCommandLine.groovy 
b/src/main/groovy/cli/EnhancedCommandLine.groovy
deleted file mode 100644
index 2f46bcb..0000000
--- a/src/main/groovy/cli/EnhancedCommandLine.groovy
+++ /dev/null
@@ -1,52 +0,0 @@
-package groovy.cli
-
-import org.apache.commons.cli.CommandLine
-import org.codehaus.groovy.runtime.StringGroovyMethods
-import org.apache.commons.cli.Option as CliOption
-import java.lang.reflect.Array
-
-class EnhancedCommandLine {
-    @Delegate CommandLine delegate
-
-    public <T> T getOptionValue(TypedOption<T> typedOption) {
-        getOptionValue(typedOption, null)
-    }
-
-    public <T> T getOptionValue(TypedOption<T> typedOption, T defaultValue) {
-        String optionName = (String) typedOption.get("longOpt")
-        if (delegate.hasOption(optionName)) {
-            return getTypedValueFromName(optionName)
-        }
-        return defaultValue
-    }
-
-    public <T> T[] getOptionValues(TypedOption<T> typedOption) {
-        String optionName = (String) typedOption.get("longOpt")
-        CliOption option = delegate.options.find{ it.longOpt == optionName }
-        T[] result = null
-        if (option) {
-            Object type = option.getType()
-            int count = 0
-            def optionValues = delegate.getOptionValues(optionName)
-            for (String optionValue : optionValues) {
-                if (result == null) {
-                    result = (T[]) Array.newInstance((Class<?>) type, 
optionValues.length)
-                }
-                result[count++] = (T) getTypedValue(type, optionValue)
-            }
-        }
-        return result
-    }
-
-    private <T> T getTypedValueFromName(String optionName) {
-        CliOption option = delegate.options.find{ it.longOpt == optionName }
-        if (!option) return null
-        Object type = option.getType()
-        String optionValue = delegate.getOptionValue(optionName)
-        return (T) getTypedValue(type, optionValue)
-    }
-
-    private static <T> T getTypedValue(Object type, String optionValue) {
-        return StringGroovyMethods.asType(optionValue, (Class<T>) type)
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/main/groovy/cli/Option.java
----------------------------------------------------------------------
diff --git a/src/main/groovy/cli/Option.java b/src/main/groovy/cli/Option.java
index 83c50fc..619f65c 100644
--- a/src/main/groovy/cli/Option.java
+++ b/src/main/groovy/cli/Option.java
@@ -18,13 +18,16 @@
  */
 package groovy.cli;
 
+import groovy.transform.Undefined;
+
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+// TODO: why setter methods?
 /**
- * Indicates that a method is a CLI option.
+ * Indicates that a method or field can be used to set a CLI option.
  */
 @java.lang.annotation.Documented
 @Retention(RetentionPolicy.RUNTIME)
@@ -38,30 +41,66 @@ public @interface Option {
     String description() default "";
 
     /**
-     * The short name of this option
+     * The short name of this option. Defaults to the name of member being 
annotated.
      *
      * @return the short name of this option
      */
     String shortName() default "";
+    // TODO: default to '_' or at least support this?
 
     /**
-     * The long name of this option
+     * The long name of this option. Defaults to the name of member being 
annotated.
      *
      * @return the long name of this option
      */
     String longName() default "";
 
     /**
-     * The value separator for this multi-valued option
+     * The value separator for this multi-valued option. Only allowed for 
array-typed arguments.
      *
      * @return the value separator for this multi-valued option
      */
     char valueSeparator() default 0;
 
     /**
-     * The default value for this option
+     * Whether this option can have an optional argument.
+     * Only supported for array-typed arguments to indicate that the array may 
be empty.
+     *
+     * @return true if this array-typed option can have an optional argument 
(i.e. could be empty)
+     */
+    boolean optionalArg() default false;
+
+    /**
+     * How many arguments this option has.
+     * A value greater than 1 is only allowed for array-typed arguments.
+     * Ignored for boolean options or if {@code numberOfArgumentsString} is 
set.
+     *
+     * @return the number of arguments
+     */
+    int numberOfArguments() default 1;
+    // TODO: 0 checked for boolean? What about def - Object as flag?
+
+    /**
+     * How many arguments this option has represented as a String.
+     * Only allowed for array-typed arguments.
+     * Overrides {@code numberOfArguments} if set.
+     * The special values of '+' means one or more and '*' as 0 or more.
+     *
+     * @return the number of arguments (as a String)
+     */
+    String numberOfArgumentsString() default "";
+
+    /**
+     * The default value for this option as a String; subject to type 
conversion and 'convert'.
      *
      * @return the default value for this option
      */
     String defaultValue() default "";
+
+    /**
+     * A conversion closure to convert the incoming String into the desired 
object
+     *
+     * @return the closure to convert this option's argument(s)
+     */
+    Class convert() default Undefined.CLASS.class; // TODO rename convert to 
handler?
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/main/groovy/util/CliBuilder.groovy
----------------------------------------------------------------------
diff --git a/src/main/groovy/util/CliBuilder.groovy 
b/src/main/groovy/util/CliBuilder.groovy
index 6e39eb9..94c22fb 100644
--- a/src/main/groovy/util/CliBuilder.groovy
+++ b/src/main/groovy/util/CliBuilder.groovy
@@ -18,10 +18,11 @@
  */
 package groovy.util
 
-import groovy.cli.EnhancedCommandLine
+import groovy.cli.CliBuilderException
 import groovy.cli.Option
 import groovy.cli.TypedOption
 import groovy.cli.Unparsed
+import groovy.transform.Undefined
 import org.apache.commons.cli.CommandLine
 import org.apache.commons.cli.CommandLineParser
 import org.apache.commons.cli.DefaultParser
@@ -35,6 +36,7 @@ import org.codehaus.groovy.runtime.MetaClassHelper
 import org.codehaus.groovy.runtime.StringGroovyMethods
 
 import java.lang.annotation.Annotation
+import java.lang.reflect.Array
 import java.lang.reflect.Field
 import java.lang.reflect.Method
 
@@ -174,18 +176,16 @@ import java.lang.reflect.Method
  * <pre>
  *   argName:        String
  *   longOpt:        String
- *   args:           int
+ *   args:           int or String
  *   optionalArg:    boolean
  *   required:       boolean
- *   type:           Object (not currently used)
+ *   type:           Class
  *   valueSeparator: char
+ *   convert:        Closure
  * </pre>
- * See {@link org.apache.commons.cli.Option} for the meaning of these 
properties
+ * See {@link org.apache.commons.cli.Option} for the meaning of most of these 
properties
  * and {@link CliBuilderTest} for further examples.
  * <p>
- *
- * @author Dierk Koenig
- * @author Paul King
  */
 class CliBuilder {
 
@@ -261,16 +261,29 @@ class CliBuilder {
     def invokeMethod(String name, Object args) {
         if (args instanceof Object[]) {
             if (args.size() == 1 && (args[0] instanceof String || args[0] 
instanceof GString)) {
-                options.addOption(option(name, [:], args[0]))
-                return null
+                def option = option(name, [:], args[0])
+                options.addOption(option)
+
+                return create(option, null, null, null)
             }
             if (args.size() == 1 && args[0] instanceof CliOption && name == 
'leftShift') {
-                options.addOption(args[0])
-                return null
+                CliOption option = args[0]
+                options.addOption(option)
+                return create(option, null, null, null)
             }
             if (args.size() == 2 && args[0] instanceof Map) {
-                options.addOption(option(name, args[0], args[1]))
-                return null
+                def convert = args[0].remove('convert')
+                def type = args[0].remove('type')
+                if (type && !(type instanceof Class)) {
+                    throw new CliBuilderException("'type' must be a Class")
+                }
+                if ((convert || type) && !args[0].containsKey('args') &&
+                        type?.simpleName?.toLowerCase() != 'boolean') {
+                    args[0].args = 1
+                }
+                def option = option(name, args[0], args[1])
+                options.addOption(option)
+                return create(option, type, null, convert)
             }
         }
         return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args)
@@ -286,7 +299,10 @@ class CliBuilder {
             parser = posix != null && posix == false ? new GnuParser() : new 
DefaultParser()
         }
         try {
-            return new OptionAccessor(new EnhancedCommandLine(delegate: 
parser.parse(options, args as String[], stopAtNonOption)))
+            def accessor = new OptionAccessor(
+                    parser.parse(options, args as String[], stopAtNonOption))
+            accessor.savedTypeOptions = savedTypeOptions
+            return accessor
         } catch (ParseException pe) {
             writer.println("error: " + pe.message)
             usage()
@@ -302,6 +318,14 @@ class CliBuilder {
         writer.flush()
     }
 
+    /**
+     * Given an interface containing members with annotations, derive
+     * the options specification.
+     *
+     * @param optionsClass
+     * @param args
+     * @return an instance containing the processed options
+     */
     public <T> T parseFromSpec(Class<T> optionsClass, String[] args) {
         addOptionsFromAnnotations(optionsClass, false)
         def cli = parse(args)
@@ -310,8 +334,16 @@ class CliBuilder {
         cliOptions as T
     }
 
+    /**
+     * Given an instance containing members with annotations, derive
+     * the options specification.
+     *
+     * @param optionInstance
+     * @param args
+     * @return the options instance populated with the processed options
+     */
     public <T> T parseFromInstance(T optionInstance, args) {
-        addOptionsFromAnnotations(options.getClass(), true)
+        addOptionsFromAnnotations(optionInstance.getClass(), true)
         def cli = parse(args)
         setOptionsFromAnnotations(cli, optionInstance.getClass(), 
optionInstance, true)
         optionInstance
@@ -320,45 +352,102 @@ class CliBuilder {
     void addOptionsFromAnnotations(Class optionClass, boolean namesAreSetters) 
{
         optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method 
m ->
             Annotation annotation = m.getAnnotation(Option)
-            options.addOption(processAddAnnotation(annotation, m, 
namesAreSetters))
+            def typedOption = processAddAnnotation(annotation, m, 
namesAreSetters)
+            options.addOption(typedOption.cliOption)
+        }
+
+        def optionFields = optionClass.declaredFields.findAll { 
it.getAnnotation(Option) }
+        if (optionClass.isInterface() && !optionFields.isEmpty()) {
+            throw new CliBuilderException("@Option only allowed on methods in 
interface " + optionClass.simpleName)
         }
-        optionClass.declaredFields.findAll{ it.getAnnotation(Option) }.each { 
Field f ->
+        optionFields.each { Field f ->
             Annotation annotation = f.getAnnotation(Option)
             String setterName = "set" + 
MetaClassHelper.capitalize(f.getName());
             Method m = optionClass.getMethod(setterName, f.getType())
-            options.addOption(processAddAnnotation(annotation, m, true))
+            def typedOption = processAddAnnotation(annotation, m, true)
+            options.addOption(typedOption.cliOption)
         }
     }
 
-    private CliOption processAddAnnotation(Option annotation, Method m, 
boolean namesAreSetters) {
+    private TypedOption processAddAnnotation(Option annotation, Method m, 
boolean namesAreSetters) {
         String shortName = annotation.shortName()
         String description = annotation.description()
         String defaultValue = annotation.defaultValue()
         char valueSeparator = annotation.valueSeparator()
+        boolean optionalArg = annotation.optionalArg()
+        Integer numberOfArguments = annotation.numberOfArguments()
+        String numberOfArgumentsString = annotation.numberOfArgumentsString()
+        Class convert = annotation.convert()
+        if (convert == Undefined.CLASS) {
+            convert = null
+        }
         String longName = adjustLongName(annotation.longName(), m, 
namesAreSetters)
         def builder = shortName && !shortName.isEmpty() ? 
CliOption.builder(shortName) : CliOption.builder()
         builder.longOpt(longName)
-        if (description && !description.isEmpty()) builder.desc(description)
-        if (defaultValue && !defaultValue.isEmpty()) 
builder.withDefaultValue(defaultValue)
+        if (numberOfArguments != 1) {
+            if (numberOfArgumentsString) {
+                throw new CliBuilderException("You can't specify both 
'numberOfArguments' and 'numberOfArgumentsString'")
+            }
+        }
+        def details = [:]
+        if (numberOfArgumentsString) {
+            details.args = numberOfArgumentsString
+            details = adjustDetails(details)
+            if (details.optionalArg) optionalArg = true
+        } else {
+            details.args = numberOfArguments
+        }
+        if (description) builder.desc(description)
         if (valueSeparator) builder.valueSeparator(valueSeparator)
         Class type = namesAreSetters ? (m.parameterTypes.size() > 0 ? 
m.parameterTypes[0] : null) : m.returnType
+        if (optionalArg && (!type || !type.isArray())) {
+            throw new CliBuilderException("Attempted to set optional argument 
for non array type")
+        }
         if (type) {
-            println "$longName $shortName $type"
-            builder.hasArg(type.simpleName.toLowerCase() != 'boolean')
-            builder.type(type)
+            def simpleNameLower = type.simpleName.toLowerCase()
+            def isFlag = simpleNameLower == 'boolean'
+            if (!isFlag) {
+                builder.hasArg(true)
+                if (details.containsKey('args')) 
builder.numberOfArgs(details.args)
+            }
+            if (type.isArray()) {
+                builder.optionalArg(optionalArg)
+            }
         }
-        def typedOption = builder.build()
-        savedTypeOptions[longName] = typedOption
+        def typedOption = create(builder.build(), convert ? null : type, 
defaultValue, convert)
         typedOption
     }
 
+    private TypedOption create(CliOption o, Class theType, defaultValue, 
convert) {
+        Map<String, Object> result = new TypedOption<Object>()
+        o.with {
+            if (opt != null) result.put("opt", opt)
+            result.put("longOpt", longOpt)
+            result.put("cliOption", o)
+            if (defaultValue && !defaultValue.isEmpty()) {
+                result.put("defaultValue", defaultValue)
+            }
+            if (convert) {
+                if (theType) {
+                    throw new CliBuilderException("You can't specify 'type' 
when using 'convert'")
+                }
+                result.put("convert", convert)
+                result.put("type", convert instanceof Class ? convert : 
convert.getClass())
+            } else {
+                result.put("type", theType)
+            }
+        }
+        savedTypeOptions[o.longOpt ?: o.opt] = result
+        result
+    }
+
     def setOptionsFromAnnotations(def cli, Class optionClass, Object t, 
boolean namesAreSetters) {
         optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method 
m ->
             Annotation annotation = m.getAnnotation(Option)
             String longName = adjustLongName(annotation.longName(), m, 
namesAreSetters)
             processSetAnnotation(m, t, longName, cli, namesAreSetters)
         }
-        optionClass.declaredFields.findAll{ it.getAnnotation(Option) }.each { 
Field f ->
+        optionClass.declaredFields.findAll { it.getAnnotation(Option) }.each { 
Field f ->
             Annotation annotation = f.getAnnotation(Option)
             String setterName = "set" + 
MetaClassHelper.capitalize(f.getName());
             Method m = optionClass.getMethod(setterName, f.getType())
@@ -385,20 +474,31 @@ class CliBuilder {
         }
     }
 
-    private void processSetAnnotation(Method m, Object t, String longName, 
cli, boolean namesAreSetters) {
+    private void processSetAnnotation(Method m, Object t, String name, cli, 
boolean namesAreSetters) {
+        def conv = savedTypeOptions[name]?.convert
+        if (conv && conv instanceof Class) {
+            savedTypeOptions[name].convert = conv.newInstance(t, t)
+        }
         if (namesAreSetters) {
             boolean isFlag = m.parameterTypes.size() > 0 && 
m.parameterTypes[0].simpleName.toLowerCase() == 'boolean'
-            if (cli.hasOption(longName) || isFlag) {
-                m.invoke(t, [isFlag ? cli.hasOption(longName) : cli[longName] 
/* cliOptions.getOptionValue(savedTypeOptions[longName]) */] as Object[])
+            if (cli.hasOption(name) || isFlag) {
+                m.invoke(t, [isFlag ? cli.hasOption(name) : optionValue(cli, 
name)] as Object[])
             }
         } else {
             boolean isFlag = m.returnType.simpleName.toLowerCase() == 'boolean'
-            t.put(m.getName(), cli.hasOption(longName) ?
-                    { -> isFlag ? true : cli[longName] /* 
cliOptions.getOptionValue(savedTypeOptions[longName]) */} :
+            t.put(m.getName(), cli.hasOption(name) ?
+                    { -> isFlag ? true : optionValue(cli, name) } :
                     { -> isFlag ? false : null })
         }
     }
 
+    private optionValue(cli, String name) {
+        if (savedTypeOptions.containsKey(name)) {
+            return cli.getOptionValue(savedTypeOptions[name])
+        }
+        cli[name]
+    }
+
     private String adjustLongName(String longName, Method m, boolean 
namesAreSetters) {
         if (!longName || longName.isEmpty()) {
             longName = m.getName()
@@ -422,10 +522,27 @@ class CliBuilder {
         } else {
             option = new CliOption(shortname, info)
         }
-        details.each { key, value -> option[key] = value }
+        adjustDetails(details).each { key, value ->
+            option[key] = value
+        }
         return option
     }
 
+    static Map adjustDetails(Map m) {
+        m.collectMany { k, v ->
+            if (k == 'args' && v == '+') {
+                [[args: org.apache.commons.cli.Option.UNLIMITED_VALUES]]
+            } else if (k == 'args' && v == '*') {
+                [[args: org.apache.commons.cli.Option.UNLIMITED_VALUES,
+                  optionalArg: true]]
+            } else if (k == 'args' && v instanceof String) {
+                [[args: Integer.parseInt(v)]]
+            } else {
+                [[(k): v]]
+            }
+        }.sum()
+    }
+
     static expandArgumentFiles(args) throws IOException {
         def result = []
         for (arg in args) {
@@ -461,58 +578,114 @@ class CliBuilder {
 }
 
 class OptionAccessor {
-    /* EnhancedCommandLine */ def inner
+    CommandLine commandLine
     Map<String, TypedOption> savedTypeOptions
 
-    OptionAccessor(inner) {
-        this.inner = inner
+    OptionAccessor(CommandLine commandLine) {
+        this.commandLine = commandLine
     }
 
     boolean hasOption(TypedOption typedOption) {
-        return inner.hasOption((String) typedOption.get("longOpt"))
+        commandLine.hasOption(typedOption.longOpt ?: typedOption.opt)
+    }
+
+    public <T> T getOptionValue(TypedOption<T> typedOption) {
+        getOptionValue(typedOption, null)
+    }
+
+    public <T> T getOptionValue(TypedOption<T> typedOption, T defaultValue) {
+        String optionName = (String) typedOption.longOpt ?: typedOption.opt
+        if (commandLine.hasOption(optionName)) {
+            if (typedOption.containsKey('type') && typedOption.type.isArray()) 
{
+                def compType = typedOption.type.componentType
+                return (T) getTypedValuesFromName(optionName, compType)
+            }
+            return getTypedValueFromName(optionName)
+        }
+        return defaultValue
+    }
+
+    private <T> T[] getTypedValuesFromName(String optionName, Class<T> 
compType) {
+        CliOption option = commandLine.options.find{ it.longOpt == optionName }
+        T[] result = null
+        if (option) {
+            int count = 0
+            def optionValues = commandLine.getOptionValues(optionName)
+            for (String optionValue : optionValues) {
+                if (result == null) {
+                    result = (T[]) Array.newInstance(compType, 
optionValues.length)
+                }
+                result[count++] = (T) getTypedValue(compType, optionName, 
optionValue)
+            }
+        }
+        if (result == null) {
+            result = (T[]) Array.newInstance(compType, 0)
+        }
+        return result
+    }
+
+    public <T> T getAt(TypedOption<T> typedOption) {
+        getAt(typedOption, null)
     }
 
     public <T> T getAt(TypedOption<T> typedOption, T defaultValue) {
-        String optionName = (String) typedOption.get("longOpt");
-        if (hasOption(optionName)) {
-            return getTypedValueFromName(optionName);
+        String optionName = (String) typedOption.longOpt ?: typedOption.opt
+        if (savedTypeOptions.containsKey(optionName)) {
+            return getTypedValueFromName(optionName)
         }
-        return defaultValue;
+        return defaultValue
     }
 
     private <T> T getTypedValueFromName(String optionName) {
-        CliOption option = savedTypeOptions.get(optionName);
-        Object type = option.getType();
-        String optionValue = inner.getOptionValue(optionName);
-        return (T) getTypedValue(type, optionValue);
+        Class type = savedTypeOptions[optionName].type
+        String optionValue = commandLine.getOptionValue(optionName)
+        return (T) getTypedValue(type, optionName, optionValue)
     }
 
-    private <T> T getTypedValue(Object type, String optionValue) {
-        return StringGroovyMethods.asType(optionValue, (Class<T>) type);
+    private <T> T getTypedValue(Class<T> type, String optionName, String 
optionValue) {
+        if (Closure.isAssignableFrom(type) && 
savedTypeOptions[optionName]?.convert) {
+            return (T) savedTypeOptions[optionName].convert(optionValue)
+        }
+        if (type?.simpleName?.toLowerCase() == 'boolean') {
+            return (T) Boolean.parseBoolean(optionValue)
+        }
+        StringGroovyMethods.asType(optionValue, (Class<T>) type)
     }
 
     def invokeMethod(String name, Object args) {
-        return InvokerHelper.getMetaClass(inner).invokeMethod(inner, name, 
args)
+        return 
InvokerHelper.getMetaClass(commandLine).invokeMethod(commandLine, name, args)
     }
 
     def getProperty(String name) {
-        def methodname = 'getParsedOptionValue'
+        def methodname = 'getOptionValue'
+        Class type = savedTypeOptions[name]?.type
+        def foundArray = type?.isArray()
         if (name.size() > 1 && name.endsWith('s')) {
             def singularName = name[0..-2]
-            if (hasOption(singularName)) {
+            if (commandLine.hasOption(singularName) || foundArray) {
                 name = singularName
                 methodname += 's'
+                type = savedTypeOptions[name]?.type
             }
         }
+        if (type?.isArray()) {
+            methodname = 'getOptionValues'
+        }
         if (name.size() == 1) name = name as char
-        def result = InvokerHelper.getMetaClass(inner).invokeMethod(inner, 
methodname, name)
-        println "result = $result for name $name, methodName $methodname"
-        if (null == result) result = inner.hasOption(name)
-        if (result instanceof String[]) result = result.toList()
+        def result = 
InvokerHelper.getMetaClass(commandLine).invokeMethod(commandLine, methodname, 
name)
+        if (result != null) {
+            if (result instanceof String[]) {
+                result = result.collect{ type ? getTypedValue(type.isArray() ? 
type.componentType : type, name, it) : it }
+            } else {
+                if (type) result = getTypedValue(type, name, result)
+            }
+        } else {
+            result = commandLine.hasOption(name)
+        }
         return result
     }
 
     List<String> arguments() {
-        inner.args.toList()
+        commandLine.args.toList()
     }
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/spec/doc/core-domain-specific-languages.adoc
----------------------------------------------------------------------
diff --git a/src/spec/doc/core-domain-specific-languages.adoc 
b/src/spec/doc/core-domain-specific-languages.adoc
index 6c0b16d..ff90f54 100644
--- a/src/spec/doc/core-domain-specific-languages.adoc
+++ b/src/spec/doc/core-domain-specific-languages.adoc
@@ -1084,8 +1084,13 @@ 
include::{projectdir}/subprojects/groovy-ant/{specfolder}/ant-builder.adoc[level
 ==== CliBuilder
 
 `CliBuilder` provides a compact way to specify the available parameters for a 
command-line application and then
-automatically parse the application's parameters according to that 
specification. Here is a simple example
-`Greeter.groovy` script illustrating usage:
+automatically parse the application's parameters according to that 
specification.
+Even though the details of each command-line you create could be quite 
different, the same main steps are
+followed each time. First, a `CliBuilder` instance is created. Then, allowed 
command-line parameters are defined.
+The arguments are then parsed according to the parameter specification 
resulting in a
+collection of options which are then interrogated.
+
+Here is a simple example `Greeter.groovy` script illustrating usage:
 
 [source,groovy]
 ---------------------------
@@ -1115,7 +1120,6 @@ Running this script with no arguments, i.e.:
 
 results in the following output:
 
-[source,shell]
 ----
 Hello World
 ----
@@ -1129,7 +1133,6 @@ Running this script with `-h` as the arguments, i.e.:
 
 results in the following output:
 
-[source,shell]
 ----
 usage: groovy Greeter [option]
  -a,--audience <arg>   greeting audience
@@ -1145,11 +1148,232 @@ Running this script with `--audience Groovologist` as 
the arguments, i.e.:
 
 results in the following output:
 
-[source,shell]
 ----
 Hello Groovologist
 ----
 
+When creating the `CliBuilder` instance in the above example, we used the 
optional `usage` named
+parameter to the constructor. This follows Groovy's normal ability to set 
additional properties
+of the instance during construction. There are numerous other properties which 
can be set
+such as `header` and `footer`. For the complete set of available properties, 
see the
+available properties for the gapi:groovy.util.CliBuilder[CliBuilder] class.
+
+When defining an allowed command-line parameter, a short description must be 
supplied, e.g.
+"display usage" for the `help` option shown previously. In our example above, 
we also used some
+additional named parameters such as `longOpt` and `args`. The following 
additional named
+parameters are supported when specifying an allowed command-line parameter:
+
+[options="header"]
+|======================
+| Name           | Description | Type
+| argName        | the name of the argument for this option used in output | 
String
+| longOpt        | the long representation of the option | String
+| args           | the number of argument values | int
+| optionalArg    | whether the argument value is optional | boolean
+| required       | whether the option is mandatory | boolean
+| type           | the type of this option | Class
+| valueSeparator | the character that is the value separator | char
+|======================
+
+If you have an option with only a `longOpt` variant, you can use the special 
shortname of 'pass:[_]'
+to specify the option, e.g. : `cli.pass:[_](longOpt: 'verbose', 'enable 
verbose logging')`.
+Some of the remaining named parameters should be fairly self-explanatory while 
others deserve
+a bit more explanation. But before further explanations, let's look at ways of 
using
+`CliBuilder` with annotations.
+
+===== Using Annotations and an interface
+
+Rather than making a series of method calls to specify the allowable options, 
you can
+provide an interface specification of the allowable options where annotations 
are used
+to indicate and provide details for the allowable options and for how 
un-processed
+arguments are handled. Two annotations are used: gapi:groovy.cli.Option[] and 
gapi:groovy.cli.Unparsed[]
+
+Here is how such a specification can be defined:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=annotationInterfaceSpec,indent=0]
+----
+<1> Specify a Boolean option set using `-h` or `--help`
+<2> Specify a String option set using `-a` or `--audience`
+<3> Specify where any remaining args will be stored
+
+Note how the long name is automatically determined from the interface method 
name.
+You can use the `longName` annotation attribute to override that behavior and 
specify
+a custom long name if you wish.
+
+Here is how you could use the interface specification:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=annotationInterface,indent=0]
+----
+<1> Create a `CliBuilder` instance as before with optional parameters
+<2> Parse arguments using the interface specification
+<3> Interrogate options using the methods from the interface
+<4> Parse a different set of arguments
+<5> Interrogate the remaining arguments
+
+When `parseFromSpec` is called, `CliBuilder` automatically creates an instance 
implementing the interface
+and populates it. You simply call the interface methods to interrogate the 
option values.
+
+===== Using Annotations and an instance
+
+Alternatively, perhaps you already have a domain class containing the option 
information.
+You can simply annotate properties or setters from that class to enable 
`CliBuilder` to appropriately
+populate your domain object.
+
+Here is how such a specification can be defined:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=annotationClassSpec,indent=0]
+----
+<1> Indicate that a Boolean property is an option
+<2> Indicate that a String property (with explicit setter) is an option
+<3> Specify where any remaining args will be stored
+
+And here is how you could use the specification:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=annotationClass,indent=0]
+----
+<1> Create a `CliBuilder` instance as before with optional parameters
+<2> Create an instance for `CliBuilder` to populate
+<3> Parse arguments populating the supplied instance
+<4> Interrogate the String option property
+<5> Interrogate the remaining arguments property
+
+When `parseFromInstance` is called, `CliBuilder` automatically populates your 
instance.
+You simply interrogate the instance properties to access the option values.
+
+===== Using Annotations and a script
+
+Finally, there are two additional convenience annotation aliases specifically 
for scripts. They
+simply combine the previously mentioned annotations and 
gapi:groovy.transform.Field[].
+The groovydoc for those annotations reveals the details: 
gapi:groovy.cli.OptionField[] and
+gapi:groovy.cli.UnparsedField[].
+
+Here is an example using those annotations in a self-contained script that 
would be called
+with the same arguments as shown for the instance example earlier:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=annotationScript,indent=0]
+----
+
+===== Options with arguments
+
+We saw in our initial example that some options act like flags, e.g. `Greeter 
-h` but
+others take an argument, e.g. `Greeter --audience Groovologist`. The simplest 
cases
+involve options which act like flags or have a single (potentially optional) 
argument.
+Here is an example involving those cases:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withArgument,indent=0]
+----
+<1> An option that is simply a flag - the default; setting args to 0 is 
allowed but not needed.
+<2> An option with exactly one argument
+<3> An option with an optional argument; it acts like a flag if the option is 
left out
+<4> An example using this spec where an argument is supplied to the 'c' option
+<5> An example using this spec where no argument is supplied to the 'c' 
option; it's just a flag
+
+Note: when an option with an optional argument is encountered, it will 
(somewhat) greedily consume the
+next argument on the commandline. If however, the next argument matches a 
known long or short
+option (with leading single or double hyphens), that will take precedence, 
e.g. `-b` in the above example.
+
+Arguments may also be specified using the annotation style. Here is an 
interface option specification:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withArgumentInterfaceSpec,indent=0]
+----
+
+And here is how it is used:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withArgumentInterface,indent=0]
+----
+
+This example makes use of an array-typed option specification. We cover this 
in more detail next when we discuss
+multiple arguments.
+
+===== Options with multiple arguments
+
+Multiple arguments are also supported using an args value greater than 1. 
There is a special named parameter,
+`valueSeparator`, which can also be optionally used when processing multiple 
arguments. It allows some additional
+flexibility in the syntax supported when supplying such argument lists on the 
commandline. For example,
+supplying a value separator of ',' allows a comma-delimited list of values to 
be passed on the commandline.
+
+The `args` value is normally an integer. It can be optionally supplied as a 
String. There are two special
+String symbols: `+` and `*`. The `*` value means 0 or more. The `+` value 
means 1 or more. The `*` value is
+the same as using `+` and also setting the `optionalArg` value to true.
+
+Accessing the multiple arguments follows a special convention. Simply add an 
's' to the normal property
+you would use to access the argument option and you will retrieve all the 
supplied arguments as a list.
+So, for a short option named 'a', you access the first 'a' argument using 
`options.a` and the list of
+all arguments using `options.as`. It's fine to have a shortname or longname 
ending in 's' so long as you
+don't also have the singular variant without the 's'. So, if `name` is one of 
your options with multiple arguments
+and `guess` is another with a single argument, there will be no confusion 
using `options.names` and `options.guess`.
+
+Here is an excerpt highlighting the use of multiple arguments:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=multipleArgs,indent=0]
+----
+<1> Args value supplied as a String and comma value separator specified
+<2> One or more arguments are allowed
+<3> Two commandline parameters will be supplied as the 'b' option's list of 
arguments
+<4> Access the 'a' option's first argument
+<5> Access the 'a' option's list of arguments
+<6> An alternative syntax for specifying two arguments for the 'a' option
+<7> The arguments to the 'b' option supplied as a comma-separated value
+
+As an alternative to obtaining multiple arguments is to use an array-based 
type for the
+option. In this case, all options will always be returned via the array. We'll 
see an example
+of this next when discussing types. It is also the only approach to use when 
using the annotation style.
+
+===== Specifying a type
+
+When an explicit type is defined, the `args` named-parameter is assumed to be 
1 (except for Boolean-typed
+options where it is 0 by default). An explicit `args` parameter can still be 
provided if needed.
+Here is an example using types:
+
+[source,groovy]
+----
+include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withType,indent=0]
+----
+<1> For an array type, the trailing 's' can be used but isn't needed
+
+Primitives, numeric types, files, enums and arrays thereof, are supported 
(they are converted using
+gapi:org.codehaus.groovy.runtime.StringGroovyMethods#asType[StringGroovyMethods#asType(String,
 Class)]).
+
+===== Setting a default value
+
+===== Custom parsing of the argument String
+
+===== Advanced CLI Usage
+
+[NOTE]
+===============================
+*NOTE* Advanced CLI features
+
+`CliBuilder` can be thought of a Groovy friendly wrapper on top of (currently) 
Apache Commons CLI.
+If there is a feature not provided by `CliBuilder` that you know is supported 
in the underlying
+library, the current `CliBuilder` implementation (and various Groovy language 
features) make it easy for you
+to call the underlying library methods directly. Doing so is a pragmatic way 
to leverage the Groovy-friendly
+syntax offered by `CliBuilder` and yet still access some of the underlying 
library's advanced features.
+A word of caution however; future versions of `CliBuilder` could potentially 
use another underlying library
+and in that event, some porting work may be required for your Groovy classes 
and/or scripts.
+===============================
+
+
+===== Use with `TypeChecked` and `CompileStatic`
+
 ==== ObjectGraphBuilder
 
 `ObjectGraphBuilder` is a builder for an arbitrary graph of beans that

http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/spec/test/builder/CliBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/src/spec/test/builder/CliBuilderTest.groovy 
b/src/spec/test/builder/CliBuilderTest.groovy
new file mode 100644
index 0000000..091ce7c
--- /dev/null
+++ b/src/spec/test/builder/CliBuilderTest.groovy
@@ -0,0 +1,247 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package builder
+
+import groovy.cli.Option
+import groovy.cli.Unparsed
+
+import java.math.RoundingMode
+
+//import java.math.RoundingMode
+
+class CliBuilderTest extends GroovyTestCase {
+//    void tearDown() {
+//    }
+
+    // tag::annotationInterfaceSpec[]
+    interface GreeterI {
+        @Option(shortName='h', description='display usage') Boolean help()     
   // <1>
+        @Option(shortName='a', description='greeting audience') String 
audience() // <2>
+        @Unparsed List remaining()                                             
   // <3>
+    }
+    // end::annotationInterfaceSpec[]
+
+    // tag::annotationClassSpec[]
+    class GreeterC {
+        @Option(shortName='h', description='display usage')
+        Boolean help                        // <1>
+
+        private String audience
+        @Option(shortName='a', description='greeting audience')
+        void setAudience(String audience) { // <2>
+            this.audience = audience
+        }
+        String getAudience() { audience }
+
+        @Unparsed
+        List remaining                      // <3>
+    }
+    // end::annotationClassSpec[]
+
+    void testAnnotationsInterface() {
+        // tag::annotationInterface[]
+        def cli = new CliBuilder(usage: 'groovy Greeter [option]')  // <1>
+        def argz = '--audience Groovologist'.split()
+        def options = cli.parseFromSpec(GreeterI, argz)             // <2>
+        assert options.audience() == 'Groovologist'                 // <3>
+
+        argz = '-h Some Other Args'.split()
+        options = cli.parseFromSpec(GreeterI, argz)                 // <4>
+        assert options.help()
+        assert options.remaining() == ['Some', 'Other', 'Args']     // <5>
+        // end::annotationInterface[]
+    }
+
+    void testAnnotationsClass() {
+        // tag::annotationClass[]
+        def cli = new CliBuilder(usage: 'groovy Greeter [option]') // <1>
+        def options = new GreeterC()                               // <2>
+        def argz = '--audience Groovologist foo'.split()
+        cli.parseFromInstance(options, argz)                       // <3>
+        assert options.audience == 'Groovologist'                  // <4>
+        assert options.remaining == ['foo']                        // <5>
+        // end::annotationClass[]
+    }
+
+    void testParseScript() {
+        def argz = '--audience Groovologist foo'.split()
+        new GroovyShell().run('''
+            // tag::annotationScript[]
+            import groovy.cli.OptionField
+            import groovy.cli.UnparsedField
+
+            @OptionField String audience
+            @OptionField Boolean help
+            @UnparsedField List remaining
+            new CliBuilder().parseFromInstance(this, args)
+            assert audience == 'Groovologist'
+            assert remaining == ['foo']
+            // end::annotationScript[]
+        ''', 'TestScript.groovy', argz)
+    }
+
+    void testWithArgument() {
+        // tag::withArgument[]
+        def cli = new CliBuilder()
+        cli.a(args: 0, 'a arg') // <1>
+        cli.b(args: 1, 'b arg') // <2>
+        cli.c(args: 1, optionalArg: true, 'c arg') // <3>
+        def options = cli.parse('-a -b foo -c bar baz'.split()) // <4>
+
+        assert options.a == true
+        assert options.b == 'foo'
+        assert options.c == 'bar'
+        assert options.arguments() == ['baz']
+
+        options = cli.parse('-a -c -b foo bar baz'.split()) // <5>
+
+        assert options.a == true
+        assert options.c == true
+        assert options.b == 'foo'
+        assert options.arguments() == ['bar', 'baz']
+        // end::withArgument[]
+    }
+
+    // tag::withArgumentInterfaceSpec[]
+    interface WithArgsI {
+        @Option boolean a()
+        @Option String b()
+        @Option(optionalArg=true) String[] c()
+        @Unparsed List remaining()
+    }
+    // end::withArgumentInterfaceSpec[]
+
+    void testWithArgumentInterface() {
+        // tag::withArgumentInterface[]
+        def cli = new CliBuilder()
+        def options = cli.parseFromSpec(WithArgsI, '-a -b foo -c bar 
baz'.split())
+        assert options.a()
+        assert options.b() == 'foo'
+        assert options.c() == ['bar']
+        assert options.remaining() == ['baz']
+
+        options = cli.parseFromSpec(WithArgsI, '-a -c -b foo bar baz'.split())
+        assert options.a()
+        assert options.c() == []
+        assert options.b() == 'foo'
+        assert options.remaining() == ['bar', 'baz']
+        // end::withArgumentInterface[]
+    }
+
+    void testMultipleArgsAndOptionalValueSeparator() {
+        // tag::multipleArgs[]
+        def cli = new CliBuilder()
+        cli.a(args: 2, 'a-arg')
+        cli.b(args: '2', valueSeparator: ',', 'b-arg') // <1>
+        cli.c(args: '+', valueSeparator: ',', 'c-arg') // <2>
+
+        def options = cli.parse('-a 1 2 3 4'.split()) // <3>
+        assert options.a == '1' // <4>
+        assert options.as == ['1', '2'] // <5>
+        assert options.arguments() == ['3', '4']
+
+        options = cli.parse('-a1 -a2 3'.split()) // <6>
+        assert options.as == ['1', '2']
+        assert options.arguments() == ['3']
+
+        options = cli.parse(['-b1,2']) // <7>
+        assert options.bs == ['1', '2']
+
+        options = cli.parse(['-c', '1'])
+        assert options.cs == ['1']
+
+        options = cli.parse(['-c1'])
+        assert options.cs == ['1']
+
+        options = cli.parse(['-c1,2,3'])
+        assert options.cs == ['1', '2', '3']
+        // end::multipleArgs[]
+    }
+
+    void testType() {
+        // tag::withType[]
+        def argz = '''-a John -b -d 21 -e 1980 -f 3.5 -g 3.14159
+            -h cv.txt -i DOWN -j 3 4 5 -k1.5,2.5,3.5 and some more'''.split()
+        def cli = new CliBuilder()
+        cli.a(type: String, 'a-arg')
+        cli.b(type: boolean, 'b-arg')
+        cli.c(type: Boolean, 'c-arg')
+        cli.d(type: int, 'd-arg')
+        cli.e(type: Long, 'e-arg')
+        cli.f(type: Float, 'f-arg')
+        cli.g(type: BigDecimal, 'g-arg')
+        cli.h(type: File, 'h-arg')
+        cli.i(type: RoundingMode, 'i-arg')
+        cli.j(args: 3, type: int[], 'j-arg')
+        cli.k(args: '+', valueSeparator: ',', type: BigDecimal[], 'k-arg')
+        def options = cli.parse(argz)
+        assert options.a == 'John'
+        assert options.b
+        assert !options.c
+        assert options.d == 21
+        assert options.e == 1980L
+        assert options.f == 3.5f
+        assert options.g == 3.14159
+        assert options.h == new File('cv.txt')
+        assert options.i == RoundingMode.DOWN
+        assert options.js == [3, 4, 5] // <1>
+        assert options.j == [3, 4, 5]  // <1>
+        assert options.k == [1.5, 2.5, 3.5]
+        assert options.arguments() == ['and', 'some', 'more']
+        // end::withType[]
+    }
+
+    void testConvert() {
+        // tag::withConvert[]
+        def argz = '''-a John -b Mary -d 2016-01-01 and some more'''.split()
+        def cli = new CliBuilder()
+        def lower = { it.toLowerCase() }
+        cli.a(convert: lower, 'a-arg')
+        cli.b(convert: { it.toUpperCase() }, 'b-arg')
+        cli.d(convert: { Date.parse('yyyy-MM-dd', it) }, 'd-arg')
+        def options = cli.parse(argz)
+        assert options.a == 'john'
+        assert options.b == 'MARY'
+        assert options.d.format('dd-MMM-yyyy') == '01-Jan-2016'
+        assert options.arguments() == ['and', 'some', 'more']
+        // end::withConvert[]
+    }
+
+    // tag::withConvertInterfaceSpec[]
+    interface WithConvertI {
+        @Option(convert={ it.toLowerCase() }) String a()
+        @Option(convert={ it.toUpperCase() }) String b()
+        @Option(convert={ Date.parse("yyyy-MM-dd", it) }) Date d()
+        @Unparsed List remaining()
+    }
+    // end::withConvertInterfaceSpec[]
+
+    void testConvertInterface() {
+        // tag::withConvertInterface[]
+        def newYears = Date.parse("yyyy-MM-dd", "2016-01-01")
+        def argz = '''-a John -b Mary -d 2016-01-01 and some more'''.split()
+        def cli = new CliBuilder()
+        def options = cli.parseFromSpec(WithConvertI, argz)
+        assert options.a() == 'john'
+        assert options.b() == 'MARY'
+        assert options.d() == newYears
+        assert options.remaining() == ['and', 'some', 'more']
+        // end::withConvertInterface[]
+    }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/09f137b0/src/test/groovy/util/CliBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/groovy/util/CliBuilderTest.groovy 
b/src/test/groovy/util/CliBuilderTest.groovy
index 72fb25d..3bf47ef 100644
--- a/src/test/groovy/util/CliBuilderTest.groovy
+++ b/src/test/groovy/util/CliBuilderTest.groovy
@@ -18,12 +18,19 @@
  */
 package groovy.util
 
+import groovy.cli.Option
+import groovy.cli.Unparsed
+import groovy.transform.ToString
 import org.apache.commons.cli.BasicParser
 import org.apache.commons.cli.DefaultParser
 import org.apache.commons.cli.GnuParser
-import org.apache.commons.cli.Option
 import org.codehaus.groovy.cli.GroovyPosixParser
 
+import java.math.RoundingMode
+
+import static org.apache.commons.cli.Option.UNLIMITED_VALUES
+import static org.apache.commons.cli.Option.builder
+
 /**
  * Test class for the CliBuilder.
  * <p>
@@ -143,7 +150,8 @@ usage: groovy
     void testLongOptsOnly_nonOptionShouldStopArgProcessing() {
         [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { 
parser ->
             def cli = new CliBuilder(parser: parser)
-            def anOption = 
Option.builder().longOpt('anOption').hasArg().desc('An option.').build()
+            def anOption = builder().longOpt('anOption').hasArg().desc('An 
option.')
+                    .build()
             cli.options.addOption(anOption)
             def options = cli.parse(['-v', '--anOption', 'something'])
             // no options should be found
@@ -158,7 +166,7 @@ usage: groovy
     void testLongAndShortOpts_allOptionsValid() {
         [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new 
BasicParser()].each { parser ->
             def cli = new CliBuilder(parser: parser)
-            def anOption = 
Option.builder().longOpt('anOption').hasArg().desc('An option.').build()
+            def anOption = builder().longOpt('anOption').hasArg().desc('An 
option.').build()
             cli.options.addOption(anOption)
             cli.v(longOpt: 'verbose', 'verbose mode')
             def options = cli.parse(['-v', '--anOption', 'something'])
@@ -181,7 +189,7 @@ usage: groovy
     void testMultipleOccurrencesSeparateSeparate() {
         [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new 
BasicParser()].each { parser ->
             def cli = new CliBuilder(parser: parser)
-            cli.a(longOpt: 'arg', args: Option.UNLIMITED_VALUES, 'arguments')
+            cli.a(longOpt: 'arg', args: UNLIMITED_VALUES, 'arguments')
             def options = cli.parse(['-a', '1', '-a', '2', '-a', '3'])
             assertEquals('1', options.a)
             assertEquals(['1', '2', '3'], options.as)
@@ -194,7 +202,7 @@ usage: groovy
     void testMultipleOccurrencesSeparateJuxtaposed() {
         [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { 
parser ->
             def cli = new CliBuilder(parser: parser)
-            //cli.a ( longOpt : 'arg' , args : Option.UNLIMITED_VALUES , 
'arguments' )
+            //cli.a ( longOpt : 'arg' , args : UNLIMITED_VALUES , 'arguments' )
             cli.a(longOpt: 'arg', args: 1, 'arguments')
             def options = cli.parse(['-a1', '-a2', '-a3'])
             assertEquals('1', options.a)
@@ -208,7 +216,7 @@ usage: groovy
     void testMultipleOccurrencesTogetherSeparate() {
         [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { 
parser ->
             def cli = new CliBuilder(parser: parser)
-            cli.a(longOpt: 'arg', args: Option.UNLIMITED_VALUES, 
valueSeparator: ',' as char, 'arguments')
+            cli.a(longOpt: 'arg', args: UNLIMITED_VALUES, valueSeparator: ',' 
as char, 'arguments')
             def options = cli.parse(['-a 1,2,3'])
             assertEquals(' 1', options.a)
             assertEquals([' 1', '2', '3'], options.as)
@@ -221,7 +229,7 @@ usage: groovy
     void testMultipleOccurrencesTogetherJuxtaposed() {
         [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { 
parser ->
             def cli1 = new CliBuilder(parser: parser)
-            cli1.a(longOpt: 'arg', args: Option.UNLIMITED_VALUES, 
valueSeparator: ',' as char, 'arguments')
+            cli1.a(longOpt: 'arg', args: UNLIMITED_VALUES, valueSeparator: ',' 
as char, 'arguments')
             def options = cli1.parse(['-a1,2,3'])
             assertEquals('1', options.a)
             assertEquals(['1', '2', '3'], options.as)
@@ -424,4 +432,135 @@ usage: groovy
         assert options.d
         assert options.arguments() == ['foo']
     }
+
+    interface PersonI {
+        @Option String first()
+        @Option String last()
+        @Option boolean flag1()
+        @Option Boolean flag2()
+        @Option(longName = 'specialFlag') Boolean flag3()
+        @Option flag4()
+        @Option int age()
+        @Option Integer born()
+        @Option float discount()
+        @Option BigDecimal pi()
+        @Option File biography()
+        @Option RoundingMode roundingMode()
+        @Unparsed List remaining()
+    }
+
+    def argz = "--first John --last Smith --flag1 --flag2 --specialFlag --age  
21 --born 1980 --discount 3.5 --pi 3.14159 --biography cv.txt --roundingMode 
DOWN and some more".split()
+
+    void testParseFromSpec() {
+        def builder1 = new CliBuilder()
+        def p1 = builder1.parseFromSpec(PersonI, argz)
+        assert p1.first() == 'John'
+        assert p1.last() == 'Smith'
+        assert p1.flag1()
+        assert p1.flag2()
+        assert p1.flag3()
+        assert !p1.flag4()
+        assert p1.born() == 1980
+        assert p1.age() == 21
+        assert p1.discount() == 3.5f
+        assert p1.pi() == 3.14159
+        assert p1.biography() == new File('cv.txt')
+        assert p1.roundingMode() == RoundingMode.DOWN
+        assert p1.remaining() == ['and', 'some', 'more']
+    }
+
+    @ToString(includeFields=true, excludes='metaClass', includePackage=false)
+    class PersonC {
+        @Option String first
+        private String last
+        @Option boolean flag1
+        private Boolean flag2
+        private Boolean flag3
+        private Boolean flag4
+        private int age
+        private Integer born
+        private float discount
+        private BigDecimal pi
+        private File biography
+        private RoundingMode roundingMode
+        private List remaining
+
+        @Option void setLast(String last) {
+            this.last = last
+        }
+        @Option void setFlag2(boolean flag2) {
+            this.flag2 = flag2
+        }
+        @Option(longName = 'specialFlag') void setFlag3(boolean flag3) {
+            this.flag3 = flag3
+        }
+        @Option void setFlag4(boolean flag4) {
+            this.flag4 = flag4
+        }
+        @Option void setAge(int age) {
+            this.age = age
+        }
+        @Option void setBorn(Integer born) {
+            this.born = born
+        }
+        @Option void setDiscount(float discount) {
+            this.discount = discount
+        }
+        @Option void setPi(BigDecimal pi) {
+            this.pi = pi
+        }
+        @Option void setBiography(File biography) {
+            this.biography = biography
+        }
+        @Option void setRoundingMode(RoundingMode roundingMode) {
+            this.roundingMode = roundingMode
+        }
+        @Unparsed void setRemaining(List remaining) {
+            this.remaining = remaining
+        }
+    }
+
+    void testParseFromInstance() {
+        def p2 = new PersonC()
+        def builder2 = new CliBuilder()
+        builder2.parseFromInstance(p2, argz)
+        // properties show first in toString()
+        assert p2.toString() == 'CliBuilderTest$PersonC(John, true, Smith, 
true, true, false, 21, 1980, 3.5, 3.14159,' +
+                ' cv.txt, DOWN, [and, some, more])'
+    }
+
+    void testParseScript() {
+        new GroovyShell().run('''
+            import groovy.cli.OptionField
+            import groovy.cli.UnparsedField
+            import java.math.RoundingMode
+            @OptionField String first
+            @OptionField String last
+            @OptionField boolean flag1
+            @OptionField Boolean flag2
+            @OptionField(longName = 'specialFlag') Boolean flag3
+            @OptionField Boolean flag4
+            @OptionField int age
+            @OptionField Integer born
+            @OptionField float discount
+            @OptionField BigDecimal pi
+            @OptionField File biography
+            @OptionField RoundingMode roundingMode
+            @UnparsedField List remaining
+            new CliBuilder().parseFromInstance(this, args)
+            assert first == 'John'
+            assert last == 'Smith'
+            assert flag1
+            assert flag2
+            assert flag3
+            assert !flag4
+            assert born == 1980
+            assert age == 21
+            assert discount == 3.5f
+            assert pi == 3.14159
+            assert biography == new File('cv.txt')
+            assert roundingMode == RoundingMode.DOWN
+            assert remaining == ['and', 'some', 'more']
+        ''', 'CliBuilderTestScript.groovy', argz)
+    }
 }

Reply via email to