You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2016/04/13 12:41:31 UTC
[2/8] groovy git commit: add annotation support to CliBuilder
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)
+ }
}