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) + } }