Repository: groovy Updated Branches: refs/heads/GROOVY_2_5_X 1a13cf633 -> 144983659
http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-cli-commons/src/main/groovy/groovy/util/CliBuilder.groovy ---------------------------------------------------------------------- diff --git a/subprojects/groovy-cli-commons/src/main/groovy/groovy/util/CliBuilder.groovy b/subprojects/groovy-cli-commons/src/main/groovy/groovy/util/CliBuilder.groovy new file mode 100644 index 0000000..bc7d44a --- /dev/null +++ b/subprojects/groovy-cli-commons/src/main/groovy/groovy/util/CliBuilder.groovy @@ -0,0 +1,798 @@ +/* + * 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.util + +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 +import org.apache.commons.cli.GnuParser +import org.apache.commons.cli.HelpFormatter +import org.apache.commons.cli.Option as CliOption +import org.apache.commons.cli.Options +import org.apache.commons.cli.ParseException +import org.codehaus.groovy.runtime.InvokerHelper +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 + +/** + * Provides a builder to assist the processing of command line arguments. + * Two styles are supported: dynamic api style (declarative method calls provide a mini DSL for describing options) + * and annotation style (annotations on an interface or class describe options). + * <p> + * <b>Dynamic api style</b> + * <p> + * Typical usage (emulate partial arg processing of unix command: ls -alt *.groovy): + * <pre> + * def cli = new CliBuilder(usage:'ls') + * cli.a('display all files') + * cli.l('use a long listing format') + * cli.t('sort by modification time') + * def options = cli.parse(args) + * assert options // would be null (false) on failure + * assert options.arguments() == ['*.groovy'] + * assert options.a && options.l && options.t + * </pre> + * The usage message for this example (obtained using <code>cli.usage()</code>) is shown below: + * <pre> + * usage: ls + * -a display all files + * -l use a long listing format + * -t sort by modification time + * </pre> + * An underlying parser that supports what is called argument 'bursting' is used + * by default. Bursting would convert '-alt' into '-a -l -t' provided no long + * option exists with value 'alt' and provided that none of 'a', 'l' or 't' + * takes an argument (in fact the last one is allowed to take an argument). + * The bursting behavior can be turned off by using an + * alternate underlying parser. The simplest way to achieve this is by using + * the deprecated GnuParser from Commons CLI with the parser property on the CliBuilder, + * i.e. include <code>parser: new GnuParser()</code> in the constructor call. + * <p> + * Another example (partial emulation of arg processing for 'ant' command line): + * <pre> + * def cli = new CliBuilder(usage:'ant [options] [targets]', + * header:'Options:') + * cli.help('print this message') + * cli.logfile(args:1, argName:'file', 'use given file for log') + * cli.D(args:2, valueSeparator:'=', argName:'property=value', + * 'use value for given property') + * def options = cli.parse(args) + * ... + * </pre> + * Usage message would be: + * <pre> + * usage: ant [options] [targets] + * Options: + * -D <property=value> use value for given property + * -help print this message + * -logfile <file> use given file for log + * </pre> + * And if called with the following arguments '-logfile foo -Dbar=baz target' + * then the following assertions would be true: + * <pre> + * assert options // would be null (false) on failure + * assert options.arguments() == ['target'] + * assert options.Ds == ['bar', 'baz'] + * assert options.logfile == 'foo' + * </pre> + * Note the use of some special notation. By adding 's' onto an option + * that may appear multiple times and has an argument or as in this case + * uses a valueSeparator to separate multiple argument values + * causes the list of associated argument values to be returned. + * <p> + * Another example showing long options (partial emulation of arg processing for 'curl' command line): + * <pre> + * def cli = new CliBuilder(usage:'curl [options] <url>') + * cli._(longOpt:'basic', 'Use HTTP Basic Authentication') + * cli.d(longOpt:'data', args:1, argName:'data', 'HTTP POST data') + * cli.G(longOpt:'get', 'Send the -d data with a HTTP GET') + * cli.q('If used as the first parameter disables .curlrc') + * cli._(longOpt:'url', args:1, argName:'URL', 'Set URL to work with') + * </pre> + * Which has the following usage message: + * <pre> + * usage: curl [options] <url> + * --basic Use HTTP Basic Authentication + * -d,--data <data> HTTP POST data + * -G,--get Send the -d data with a HTTP GET + * -q If used as the first parameter disables .curlrc + * --url <URL> Set URL to work with + * </pre> + * This example shows a common convention. When mixing short and long names, the + * short names are often one character in size. One character options with + * arguments don't require a space between the option and the argument, e.g. + * <code>-Ddebug=true</code>. The example also shows + * the use of '_' when no short option is applicable. + * <p> + * Also note that '_' was used multiple times. This is supported but if + * any other shortOpt or any longOpt is repeated, then the behavior is undefined. + * <p> + * Short option names may not contain a hyphen. If a long option name contains a hyphen, e.g. '--max-wait' then you can either + * use the long hand method call <code>options.hasOption('max-wait')</code> or surround + * the option name in quotes, e.g. <code>options.'max-wait'</code>. + * <p> + * Although CliBuilder on the whole hides away the underlying library used + * for processing the arguments, it does provide some hooks which let you + * make use of the underlying library directly should the need arise. For + * example, the last two lines of the 'curl' example above could be replaced + * with the following: + * <pre> + * import org.apache.commons.cli.* + * ... as before ... + * cli << new Option('q', false, 'If used as the first parameter disables .curlrc') + * cli << Option.builder().longOpt('url').hasArg().argName('URL'). + * desc('Set URL to work with').build() + * ... + * </pre> + * + * CliBuilder also supports Argument File processing. If an argument starts with + * an '@' character followed by a filename, then the contents of the file with name + * filename are placed into the command line. The feature can be turned off by + * setting expandArgumentFiles to false. If turned on, you can still pass a real + * parameter with an initial '@' character by escaping it with an additional '@' + * symbol, e.g. '@@foo' will become '@foo' and not be subject to expansion. As an + * example, if the file temp.args contains the content: + * <pre> + * -arg1 + * paramA + * paramB paramC + * </pre> + * Then calling the command line with: + * <pre> + * someCommand @temp.args -arg2 paramD + * </pre> + * Is the same as calling this: + * <pre> + * someCommand -arg1 paramA paramB paramC -arg2 paramD + * </pre> + * This feature is particularly useful on operating systems which place limitations + * on the size of the command line (e.g. Windows). The feature is similar to + * the 'Command Line Argument File' processing supported by javadoc and javac. + * Consult the corresponding documentation for those tools if you wish to see further examples. + * <p> + * <b>Supported Option Properties</b>: + * <pre> + * argName: String + * longOpt: String + * args: int or String + * optionalArg: boolean + * required: boolean + * type: Class + * valueSeparator: char + * convert: Closure + * defaultValue: String + * </pre> + * See {@link org.apache.commons.cli.Option} for the meaning of most of these properties + * and {@link CliBuilderTest} for further examples. + * <p> + * <b>Annotation style with an interface</b> + * <p> + * With this style an interface is defined containing an annotated method for each option. + * It might look like this (following roughly the earlier 'ls' example): + * <pre> + * import groovy.cli.Option + * import groovy.cli.Unparsed + * + * interface OptionInterface { + * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all() + * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat() + * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time() + * @{@link groovy.cli.Unparsed} List remaining() + * } + * </pre> + * Then this description is supplied to CliBuilder during parsing, e.g.: + * <pre> + * def args = '-alt *.groovy'.split() // normally from commandline itself + * def cli = new CliBuilder(usage:'ls') + * def options = cli.parseFromSpec(OptionInterface, args) + * assert options.remaining() == ['*.groovy'] + * assert options.all() && options.longFormat() && options.time() + * </pre> + * <p> + * <b>Annotation style with a class</b> + * <p> + * With this style a user-supplied instance is used. Annotations on that instance's class + * members (properties and setter methods) indicate how to set options and provide the option details + * using annotation attributes. + * It might look like this (again using the earlier 'ls' example): + * <pre> + * import groovy.cli.Option + * import groovy.cli.Unparsed + * + * class OptionClass { + * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all + * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat + * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time + * @{@link groovy.cli.Unparsed} List remaining + * } + * </pre> + * Then this description is supplied to CliBuilder during parsing, e.g.: + * <pre> + * def args = '-alt *.groovy'.split() // normally from commandline itself + * def cli = new CliBuilder(usage:'ls') + * def options = new OptionClass() + * cli.parseFromInstance(options, args) + * assert options.remaining == ['*.groovy'] + * assert options.all && options.longFormat && options.time + * </pre> + */ +class CliBuilder { + + /** + * Usage summary displayed as the first line when <code>cli.usage()</code> is called. + */ + String usage = 'groovy' + + /** + * Normally set internally but allows you full customisation of the underlying processing engine. + */ + CommandLineParser parser = null + + /** + * To change from the default PosixParser to the GnuParser, set this to false. Ignored if the parser is explicitly set. + * @deprecated use the parser option instead with an instance of your preferred parser + */ + @Deprecated + Boolean posix = null + + /** + * Whether arguments of the form '{@code @}<i>filename</i>' will be expanded into the arguments contained within the file named <i>filename</i> (default true). + */ + boolean expandArgumentFiles = true + + /** + * Normally set internally but can be overridden if you want to customise how the usage message is displayed. + */ + HelpFormatter formatter = new HelpFormatter() + + /** + * Defaults to stdout but you can provide your own PrintWriter if desired. + */ + PrintWriter writer = new PrintWriter(System.out) + + /** + * Optional additional message for usage; displayed after the usage summary but before the options are displayed. + */ + String header = '' + + /** + * Optional additional message for usage; displayed after the options are displayed. + */ + String footer = '' + + /** + * Indicates that option processing should continue for all arguments even + * if arguments not recognized as options are encountered (default true). + */ + boolean stopAtNonOption = true + + /** + * Allows customisation of the usage message width. + */ + int width = HelpFormatter.DEFAULT_WIDTH + + /** + * Not normally accessed directly but full access to underlying options if needed. + */ + Options options = new Options() + + Map<String, TypedOption> savedTypeOptions = new HashMap<String, TypedOption>() + + public <T> TypedOption<T> option(Map args, Class<T> type, String description) { + def name = args.opt ?: '_' + args.type = type + args.remove('opt') + "$name"(args, description) + } + + /** + * Internal method: Detect option specification method calls. + */ + def invokeMethod(String name, Object args) { + if (args instanceof Object[]) { + if (args.size() == 1 && (args[0] instanceof String || args[0] instanceof GString)) { + 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') { + CliOption option = args[0] + options.addOption(option) + return create(option, null, null, null) + } + if (args.size() == 2 && args[0] instanceof Map) { + def convert = args[0].remove('convert') + def type = args[0].remove('type') + def defaultValue = args[0].remove('defaultValue') + 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, defaultValue, convert) + } + } + return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args) + } + + /** + * Make options accessible from command line args with parser. + * Returns null on bad command lines after displaying usage message. + */ + OptionAccessor parse(args) { + if (expandArgumentFiles) args = expandArgumentFiles(args) + if (!parser) { + parser = posix != null && posix == false ? new GnuParser() : new DefaultParser() + } + try { + 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() + return null + } + } + + /** + * Print the usage message with writer (default: System.out) and formatter (default: HelpFormatter) + */ + void usage() { + formatter.printHelp(writer, width, usage, header, options, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, footer) + 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) + def cliOptions = [:] + setOptionsFromAnnotations(cli, optionsClass, cliOptions, false) + 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(optionInstance.getClass(), true) + def cli = parse(args) + setOptionsFromAnnotations(cli, optionInstance.getClass(), optionInstance, true) + optionInstance + } + + void addOptionsFromAnnotations(Class optionClass, boolean namesAreSetters) { + optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method m -> + Annotation annotation = m.getAnnotation(Option) + 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) + } + optionFields.each { Field f -> + Annotation annotation = f.getAnnotation(Option) + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + def typedOption = processAddAnnotation(annotation, m, true) + options.addOption(typedOption.cliOption) + } + } + + private TypedOption processAddAnnotation(Option annotation, Method m, boolean namesAreSetters) { + String shortName = annotation.shortName() + String description = annotation.description() + String defaultValue = annotation.defaultValue() + char valueSeparator = 0 + if (annotation.valueSeparator()) valueSeparator = annotation.valueSeparator() as char + boolean optionalArg = annotation.optionalArg() + Integer numberOfArguments = annotation.numberOfArguments() + String numberOfArgumentsString = annotation.numberOfArgumentsString() + Class convert = annotation.convert() + if (convert == Undefined.CLASS) { + convert = null + } + Map names = calculateNames(annotation.longName(), shortName, m, namesAreSetters) + def builder = names.short ? CliOption.builder(names.short) : CliOption.builder() + if (names.long) { + builder.longOpt(names.long) + } + if (numberOfArguments != 1) { + if (numberOfArgumentsString) { + throw new CliBuilderException("You can't specify both 'numberOfArguments' and 'numberOfArgumentsString'") + } + } + def details = [:] + 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") + } + def isFlag = type.simpleName.toLowerCase() == 'boolean' + if (numberOfArgumentsString) { + details.args = numberOfArgumentsString + details = adjustDetails(details) + if (details.optionalArg) optionalArg = true + } else { + details.args = isFlag ? 0 : numberOfArguments + } + if (details?.args == 0 && !(isFlag || type.name == 'java.lang.Object')) { + throw new CliBuilderException("Flag '${names.long ?: names.short}' must be Boolean or Object") + } + if (description) builder.desc(description) + if (valueSeparator) builder.valueSeparator(valueSeparator) + if (type) { + if (isFlag && details.args == 1) { + // special flag: treat like normal not boolean expecting explicit 'true' or 'false' param + isFlag = false + } + if (!isFlag) { + builder.hasArg(true) + if (details.containsKey('args')) builder.numberOfArgs(details.args) + } + if (type.isArray()) { + builder.optionalArg(optionalArg) + } + } + 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) { + 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) + Map names = calculateNames(annotation.longName(), annotation.shortName(), m, namesAreSetters) + processSetAnnotation(m, t, names.long ?: names.short, cli, namesAreSetters) + } + 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()) + Map names = calculateNames(annotation.longName(), annotation.shortName(), m, true) + processSetAnnotation(m, t, names.long ?: names.short, cli, true) + } + def remaining = cli.arguments() + optionClass.methods.findAll{ it.getAnnotation(Unparsed) }.each { Method m -> + processSetRemaining(m, remaining, t, cli, namesAreSetters) + } + optionClass.declaredFields.findAll{ it.getAnnotation(Unparsed) }.each { Field f -> + String setterName = "set" + MetaClassHelper.capitalize(f.getName()); + Method m = optionClass.getMethod(setterName, f.getType()) + processSetRemaining(m, remaining, t, cli, namesAreSetters) + } + } + + private void processSetRemaining(Method m, remaining, Object t, cli, boolean namesAreSetters) { + def resultType = namesAreSetters ? m.parameterTypes[0] : m.returnType + def isTyped = resultType?.isArray() + def result + def type = null + if (isTyped) { + type = resultType.componentType + result = remaining.collect{ cli.getValue(type, it, null) } + } else { + result = remaining.toList() + } + if (namesAreSetters) { + m.invoke(t, isTyped ? [result.toArray(Array.newInstance(type, result.size()))] as Object[] : result) + } else { + Map names = calculateNames("", "", m, namesAreSetters) + t.put(names.long, { -> result }) + } + } + + 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) + } + boolean hasArg = savedTypeOptions[name]?.cliOption?.numberOfArgs == 1 + boolean noArg = savedTypeOptions[name]?.cliOption?.numberOfArgs == 0 + if (namesAreSetters) { + def isBoolArg = m.parameterTypes.size() > 0 && m.parameterTypes[0].simpleName.toLowerCase() == 'boolean' + boolean isFlag = (isBoolArg && !hasArg) || noArg + if (cli.hasOption(name) || isFlag || cli.defaultValue(name)) { + m.invoke(t, [isFlag ? cli.hasOption(name) : + cli.hasOption(name) ? optionValue(cli, name) : cli.defaultValue(name)] as Object[]) + } + } else { + def isBoolRetType = m.returnType.simpleName.toLowerCase() == 'boolean' + boolean isFlag = (isBoolRetType && !hasArg) || noArg + t.put(m.getName(), cli.hasOption(name) ? + { -> isFlag ? true : optionValue(cli, name) } : + { -> isFlag ? false : cli.defaultValue(name) }) + } + } + + private optionValue(cli, String name) { + if (savedTypeOptions.containsKey(name)) { + return cli.getOptionValue(savedTypeOptions[name]) + } + cli[name] + } + + private Map calculateNames(String longName, String shortName, Method m, boolean namesAreSetters) { + boolean useShort = longName == '_' + if (longName == '_') longName = "" + def result = longName + if (!longName) { + result = m.getName() + if (namesAreSetters && result.startsWith("set")) { + result = MetaClassHelper.convertPropertyName(result.substring(3)) + } + } + [long: useShort ? "" : result, short: (useShort && !shortName) ? result : shortName] + } + + // implementation details ------------------------------------- + + /** + * Internal method: How to create an option from the specification. + */ + CliOption option(shortname, Map details, info) { + CliOption option + if (shortname == '_') { + option = CliOption.builder().desc(info).longOpt(details.longOpt).build() + details.remove('longOpt') + } else { + option = new CliOption(shortname, info) + } + 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) { + if (arg && arg != '@' && arg[0] == '@') { + arg = arg.substring(1) + if (arg[0] != '@') { + expandArgumentFile(arg, result) + continue + } + } + result << arg + } + return result + } + + private static expandArgumentFile(name, args) throws IOException { + def charAsInt = { String s -> s.toCharacter() as int } + new File(name).withReader { r -> + new StreamTokenizer(r).with { + resetSyntax() + wordChars(charAsInt(' '), 255) + whitespaceChars(0, charAsInt(' ')) + commentChar(charAsInt('#')) + quoteChar(charAsInt('"')) + quoteChar(charAsInt('\'')) + while (nextToken() != StreamTokenizer.TT_EOF) { + args << sval + } + } + } + } + +} + +class OptionAccessor { + CommandLine commandLine + Map<String, TypedOption> savedTypeOptions + + OptionAccessor(CommandLine commandLine) { + this.commandLine = commandLine + } + + boolean hasOption(TypedOption typedOption) { + commandLine.hasOption(typedOption.longOpt ?: typedOption.opt) + } + + public <T> T defaultValue(String name) { + Class<T> type = savedTypeOptions[name]?.type + String value = savedTypeOptions[name]?.defaultValue() ? savedTypeOptions[name].defaultValue() : null + return (T) value ? getTypedValue(type, name, value) : null + } + + 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.longOpt ?: typedOption.opt + if (savedTypeOptions.containsKey(optionName)) { + return getTypedValueFromName(optionName) + } + return defaultValue + } + + private <T> T getTypedValueFromName(String optionName) { + Class type = savedTypeOptions[optionName].type + String optionValue = commandLine.getOptionValue(optionName) + return (T) getTypedValue(type, optionName, optionValue) + } + + private <T> T getTypedValue(Class<T> type, String optionName, String optionValue) { + if (savedTypeOptions[optionName]?.cliOption?.numberOfArgs == 0) { + return (T) commandLine.hasOption(optionName) + } + def convert = savedTypeOptions[optionName]?.convert + return getValue(type, optionValue, convert) + } + + private <T> T getValue(Class<T> type, String optionValue, Closure convert) { + if (!type) { + return (T) optionValue + } + if (Closure.isAssignableFrom(type) && convert) { + return (T) 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(commandLine).invokeMethod(commandLine, name, args) + } + + def getProperty(String name) { + if (!savedTypeOptions.containsKey(name)) { + def alt = savedTypeOptions.find{ it.value.opt == name } + if (alt) name = alt.key + } + 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 (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(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 if (type?.simpleName != 'boolean' && savedTypeOptions[name]?.defaultValue) { + result = getTypedValue(type, name, savedTypeOptions[name].defaultValue) + } else { + result = commandLine.hasOption(name) + } + return result + } + + List<String> arguments() { + commandLine.args.toList() + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-cli-commons/src/test/groovy/groovy/util/CliBuilderTest.groovy ---------------------------------------------------------------------- diff --git a/subprojects/groovy-cli-commons/src/test/groovy/groovy/util/CliBuilderTest.groovy b/subprojects/groovy-cli-commons/src/test/groovy/groovy/util/CliBuilderTest.groovy new file mode 100644 index 0000000..938c79a --- /dev/null +++ b/subprojects/groovy-cli-commons/src/test/groovy/groovy/util/CliBuilderTest.groovy @@ -0,0 +1,707 @@ +/* + * 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.util + +import groovy.cli.Option +import groovy.cli.Unparsed +import groovy.transform.ToString +import groovy.transform.TypeChecked +import org.apache.commons.cli.BasicParser +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.GnuParser +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> + * Commons CLI has a long history of different parsers with slightly differing behavior and bugs. + * In nearly all cases, we now recommend using DefaultParser. In case you have very unique circumstances + * and really need behavior that can only be supplied by one of the legacy parsers, we also include + * some test case runs against some of the legacy parsers. + */ + +class CliBuilderTest extends GroovyTestCase { + + private StringWriter stringWriter + private PrintWriter printWriter + + void setUp() { + resetPrintWriter() + } + + private final expectedParameter = 'ASCII' + private final usageString = 'groovy [option]* filename' + + private void runSample(parser, optionList) { + resetPrintWriter() + def cli = new CliBuilder(usage: usageString, writer: printWriter, parser: parser) + cli.h(longOpt: 'help', 'usage information') + cli.c(argName: 'charset', args: 1, longOpt: 'encoding', 'character encoding') + cli.i(argName: 'extension', optionalArg: true, 'modify files in place, create backup if extension is given (e.g. \'.bak\')') + def stringified = cli.options.toString() + assert stringified =~ /i=\[ option: i :: modify files in place, create backup if extension is given/ + assert stringified =~ /c=\[ option: c encoding \[ARG] :: character encoding/ + assert stringified =~ /h=\[ option: h help :: usage information/ + assert stringified =~ /encoding=\[ option: c encoding \[ARG] :: character encoding/ + assert stringified =~ /help=\[ option: h help :: usage information/ + def options = cli.parse(optionList) + assert options.hasOption('h') + assert options.hasOption('help') + assert options.h + assert options.help + if (options.h) { cli.usage() } + def expectedUsage = """usage: $usageString + -c,--encoding <charset> character encoding + -h,--help usage information + -i modify files in place, create backup if + extension is given (e.g. '.bak')""" + assertEquals(expectedUsage, stringWriter.toString().tokenize('\r\n').join('\n')) + resetPrintWriter() + cli.writer = printWriter + if (options.help) { cli.usage() } + assertEquals(expectedUsage, stringWriter.toString().tokenize('\r\n').join('\n')) + assert options.hasOption('c') + assert options.c + assert options.hasOption('encoding') + assert options.encoding + assertEquals(expectedParameter, options.getOptionValue('c')) + assertEquals(expectedParameter, options.c) + assertEquals(expectedParameter, options.getOptionValue('encoding')) + assertEquals(expectedParameter, options.encoding) + assertEquals(false, options.noSuchOptionGiven) + assertEquals(false, options.hasOption('noSuchOptionGiven')) + assertEquals(false, options.x) + assertEquals(false, options.hasOption('x')) + } + + private void resetPrintWriter() { + stringWriter = new StringWriter() + printWriter = new PrintWriter(stringWriter) + } + + void testSampleShort() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + runSample(parser, ['-h', '-c', expectedParameter]) + } + } + + void testSampleLong() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + runSample(parser, ['--help', '--encoding', expectedParameter]) + } + } + + void testSimpleArg() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + cli.a([:], '') + def options = cli.parse(['-a', '1', '2']) + assertEquals(['1', '2'], options.arguments()) + } + } + + void testMultipleArgs() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + cli.a(longOpt: 'arg', args: 2, valueSeparator: ',' as char, 'arguments') + def options = cli.parse(['-a', '1,2']) + assertEquals('1', options.a) + assertEquals(['1', '2'], options.as) + assertEquals('1', options.arg) + assertEquals(['1', '2'], options.args) + } + } + + void testFailedParsePrintsUsage() { + def cli = new CliBuilder(writer: printWriter) + cli.x(required: true, 'message') + cli.parse([]) + // NB: This test is very fragile and is bound to fail on different locales and versions of commons-cli... :-( + assert stringWriter.toString().normalize() == '''error: Missing required option: x +usage: groovy + -x message +''' + } + + void testLongOptsOnly_nonOptionShouldStopArgProcessing() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + 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 + assert options.getOptionValue('anOption') == null + assert !options.anOption + assert !options.v + // arguments should be still sitting there + assert options.arguments() == ['-v', '--anOption', 'something'] + } + } + + void testLongAndShortOpts_allOptionsValid() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + 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']) + assert options.v + assert options.getOptionValue('anOption') == 'something' + assert options.anOption == 'something' + assert !options.arguments() + } + } + + void testUnrecognizedOptions() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + cli.v(longOpt: 'verbose', 'verbose mode') + def options = cli.parse(['-x', '-yyy', '--zzz', 'something']) + assertEquals(['-x', '-yyy', '--zzz', 'something'], options.arguments()) + } + } + + void testMultipleOccurrencesSeparateSeparate() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser(), new BasicParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + 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) + assertEquals('1', options.arg) + assertEquals(['1', '2', '3'], options.args) + assertEquals([], options.arguments()) + } + } + + void testMultipleOccurrencesSeparateJuxtaposed() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + //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) + assertEquals(['1', '2', '3'], options.as) + assertEquals('1', options.arg) + assertEquals(['1', '2', '3'], options.args) + assertEquals([], options.arguments()) + } + } + + void testMultipleOccurrencesTogetherSeparate() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { parser -> + def cli = new CliBuilder(parser: parser) + 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) + assertEquals(' 1', options.arg) + assertEquals([' 1', '2', '3'], options.args) + assertEquals([], options.arguments()) + } + } + + void testMultipleOccurrencesTogetherJuxtaposed() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { parser -> + def cli1 = new CliBuilder(parser: parser) + 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) + assertEquals('1', options.arg) + assertEquals(['1', '2', '3'], options.args) + assertEquals([], options.arguments()) } + } + + /* + * Behaviour with unrecognized options. + * + * TODO: Should add the BasicParser here as well? + */ + + void testUnrecognizedOptionSilentlyIgnored_GnuParser() { + def cli = new CliBuilder(usage: usageString, writer: printWriter, parser: new GnuParser()) + def options = cli.parse(['-v']) + assertEquals('''''', stringWriter.toString().tokenize('\r\n').join('\n')) + assert !options.v + } + + private void checkNoOutput() { + assert stringWriter.toString().tokenize('\r\n').join('\n') == '''''' + } + + void testUnrecognizedOptionSilentlyIgnored_DefaultParser() { + def cli = new CliBuilder(usage: usageString, writer: printWriter, parser: new DefaultParser()) + def options = cli.parse(['-v']) + checkNoOutput() + assert !options.v + } + + void testUnrecognizedOptionTerminatesParse_GnuParser() { + def cli = new CliBuilder(usage: usageString, writer: printWriter, parser: new GnuParser()) + cli.h(longOpt: 'help', 'usage information') + def options = cli.parse(['-v', '-h']) + checkNoOutput() + assert !options.v + assert !options.h + assertEquals(['-v', '-h'], options.arguments()) + } + + void testUnrecognizedOptionTerminatesParse_DefaultParser() { + def cli = new CliBuilder(usage: usageString, writer: printWriter, parser: new DefaultParser()) + cli.h(longOpt: 'help', 'usage information') + def options = cli.parse(['-v', '-h']) + checkNoOutput() + assert !options.v + assert !options.h + assertEquals(['-v', '-h'], options.arguments()) + } + + void testMultiCharShortOpt() { + [new DefaultParser(), new GroovyPosixParser(), new GnuParser()].each { parser -> + def cli = new CliBuilder(writer: printWriter, parser: parser) + cli.abc('abc option') + cli.def(longOpt: 'defdef', 'def option') + def options = cli.parse(['-abc', '--defdef', 'ghi']) + assert options + assert options.arguments() == ['ghi'] + assert options.abc && options.def && options.defdef + checkNoOutput() + } + } + + void testArgumentBursting_DefaultParserOnly() { + def cli = new CliBuilder(writer: printWriter) + // must not have longOpt 'abc' and also no args for a or b + cli.a('a') + cli.b('b') + cli.c('c') + def options = cli.parse(['-abc', '-d']) + assert options + assert options.arguments() == ['-d'] + assert options.a && options.b && options.c && !options.d + checkNoOutput() + } + + void testLongOptEndingWithS() { + def cli = new CliBuilder() + cli.s(longOpt: 'number_of_seconds', 'a long arg that ends with an "s"') + + def options = cli.parse(['-s']) + + assert options.hasOption('s') + assert options.hasOption('number_of_seconds') + assert options.s + assert options.number_of_seconds + } + + void testArgumentFileExpansion() { + def cli = new CliBuilder(usage: 'test usage') + cli.h(longOpt: 'help', 'usage information') + cli.d(longOpt: 'debug', 'turn on debug info') + def args = ['-h', '@temp.args', 'foo', '@@baz'] + def temp = new File('temp.args') + temp.deleteOnExit() + temp.text = '-d bar' + def options = cli.parse(args) + assert options.h + assert options.d + assert options.arguments() == ['bar', 'foo', '@baz'] + } + + void testArgumentFileExpansionArgOrdering() { + def cli = new CliBuilder(usage: 'test usage') + def args = ['one', '@temp1.args', 'potato', '@temp2.args', 'four'] + def temp1 = new File('temp1.args') + temp1.deleteOnExit() + temp1.text = 'potato two' + def temp2 = new File('temp2.args') + temp2.deleteOnExit() + temp2.text = 'three potato' + def options = cli.parse(args) + assert options.arguments() == 'one potato two potato three potato four'.split() + } + + void testArgumentFileExpansionTurnedOff() { + def cli = new CliBuilder(usage: 'test usage', expandArgumentFiles:false) + cli.h(longOpt: 'help', 'usage information') + cli.d(longOpt: 'debug', 'turn on debug info') + def args = ['-h', '@temp.args', 'foo', '@@baz'] + def temp = new File('temp.args') + temp.deleteOnExit() + temp.text = '-d bar' + def options = cli.parse(args) + assert options.h + assert !options.d + assert options.arguments() == ['@temp.args', 'foo', '@@baz'] + } + + void testGStringSpecification_Groovy4621() { + def user = 'scott' + def pass = 'tiger' + def ignore = false + def longOptName = 'user' + def cli = new CliBuilder(usage: 'blah') + cli.dbusername(longOpt:"$longOptName", args: 1, "Database username [default $user]") + cli.dbpassword(args: 1, "Database password [default $pass]") + cli.i("ignore case [default $ignore]") + def args = ['-dbpassword', 'foo', '--user', 'bar', '-i'] + def options = cli.parse(args) + assert options.user == 'bar' + assert options.dbusername == 'bar' + assert options.dbpassword == 'foo' + assert options.i + } + + void testNoExpandArgsWithEmptyArg() { + def cli = new CliBuilder(expandArgumentFiles: false) + cli.parse(['something', '']) + } + + void testExpandArgsWithEmptyArg() { + def cli = new CliBuilder(expandArgumentFiles: true) + cli.parse(['something', '']) + } + + void testDoubleHyphenShortOptions() { + def cli = new CliBuilder() + cli.a([:], '') + cli.b([:], '') + def options = cli.parse(['-a', '--', '-b', 'foo']) + assert options.arguments() == ['-b', 'foo'] + } + + void testDoubleHyphenLongOptions() { + def cli = new CliBuilder() + cli._([longOpt:'alpha'], '') + cli._([longOpt:'beta'], '') + def options = cli.parse(['--alpha', '--', '--beta', 'foo']) + assert options.alpha + assert options.arguments() == ['--beta', 'foo'] + } + + void testMixedShortAndLongOptions() { + def cli = new CliBuilder() + cli.a([longOpt:'alpha', args:1], '') + cli.b([:], '') + def options = cli.parse(['-b', '--alpha', 'param', 'foo']) + assert options.a == 'param' + assert options.arguments() == ['foo'] + } + + void testMixedBurstingAndLongOptions() { + def cli = new CliBuilder() + cli.a([:], '') + cli.b([:], '') + cli.c([:], '') + cli.d([longOpt:'abacus'], '') + def options = cli.parse(['-abc', 'foo']) + assert options.a + assert options.b + assert options.c + assert options.arguments() == ['foo'] + options = cli.parse(['-abacus', 'foo']) + assert !options.a + assert !options.b + assert !options.c + 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, 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 + } + } + class DefaultValueC { + @Option(shortName='f', defaultValue='one') String from + @Option(shortName='t', defaultValue='35') int to + @Option(shortName='b') int by = 1 + } + + void testDefaultValueClass() { + def cli = new CliBuilder() + def options = new DefaultValueC() + cli.parseFromInstance(options, '-f two'.split()) + assert options.from == 'two' + assert options.to == 35 + assert options.by == 1 + + options = new DefaultValueC() + cli.parseFromInstance(options, '-t 45 --by 2'.split()) + assert options.from == 'one' + assert options.to == 45 + assert options.by == 2 + } + + class ValSepC { + @Option(numberOfArguments=2) String[] a + @Option(numberOfArgumentsString='2', valueSeparator=',') String[] b + @Option(numberOfArgumentsString='+', valueSeparator=',') String[] c + @Unparsed remaining + } + + void testValSepClass() { + def cli = new CliBuilder() + + def options = new ValSepC() + cli.parseFromInstance(options, '-a 1 2 3 4'.split()) + assert options.a == ['1', '2'] + assert options.remaining == ['3', '4'] + + options = new ValSepC() + cli.parseFromInstance(options, '-a1 -a2 3'.split()) + assert options.a == ['1', '2'] + assert options.remaining == ['3'] + + options = new ValSepC() + cli.parseFromInstance(options, ['-b1,2'] as String[]) + assert options.b == ['1', '2'] + + options = new ValSepC() + cli.parseFromInstance(options, ['-c', '1'] as String[]) + assert options.c == ['1'] + + options = new ValSepC() + cli.parseFromInstance(options, ['-c1'] as String[]) + assert options.c == ['1'] + + options = new ValSepC() + cli.parseFromInstance(options, ['-c1,2,3'] as String[]) + assert options.c == ['1', '2', '3'] + } + + class WithConvertC { + @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 + } + + void testConvertClass() { + Date 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 = new WithConvertC() + cli.parseFromInstance(options, argz) + assert options.a == 'john' + assert options.b == 'MARY' + assert options.d == newYears + assert options.remaining == ['and', 'some', 'more'] + } + + class TypeCheckedC { + @Option String name + @Option int age + @Unparsed List remaining + } + + @TypeChecked + void testTypeCheckedClass() { + def argz = "--name John --age 21 and some more".split() + def cli = new CliBuilder() + def options = new TypeCheckedC() + cli.parseFromInstance(options, argz) + String n = options.name + int a = options.age + assert n == 'John' && a == 21 + assert options.remaining == ['and', 'some', 'more'] + } + + 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])' + } + + interface RetTypeI { + @Unparsed Integer[] nums() + } + + // this feature is incubating + void testTypedUnparsedFromSpec() { + def argz = '12 34 56'.split() + def cli = new CliBuilder() + def options = cli.parseFromSpec(RetTypeI, argz) + assert options.nums() == [12, 34, 56] + } + + class RetTypeC { + @Unparsed Integer[] nums + } + + // this feature is incubating + void testTypedUnparsedFromInstance() { + def argz = '12 34 56'.split() + def cli = new CliBuilder() + def options = new RetTypeC() + cli.parseFromInstance(options, argz) + assert options.nums == [12, 34, 56] + } + + interface FlagEdgeCasesI { + @Option boolean abc() + @Option(numberOfArgumentsString='1') boolean efg() + @Option(numberOfArguments=1) ijk() + @Option(numberOfArguments=0) lmn() + @Unparsed List remaining() + } + + void testParseFromInstanceFlagEdgeCases() { + def cli = new CliBuilder() + def options = cli.parseFromSpec(FlagEdgeCasesI, '-abc -efg true --ijk foo --lmn bar baz'.split()) + + assert options.abc() && options.efg() + assert options.ijk() == 'foo' + assert options.lmn() == true + assert options.remaining() == ['bar', 'baz'] + + options = cli.parseFromSpec(FlagEdgeCasesI, '-abc -ijk cat -efg false bar baz'.split()) + assert options.abc() + assert options.ijk() == 'cat' + assert !options.efg() + assert options.lmn() == false + assert options.remaining() == ['bar', 'baz'] + } + + 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) + } +} http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-console/build.gradle ---------------------------------------------------------------------- diff --git a/subprojects/groovy-console/build.gradle b/subprojects/groovy-console/build.gradle index 6a92dc1..0e394d1 100644 --- a/subprojects/groovy-console/build.gradle +++ b/subprojects/groovy-console/build.gradle @@ -20,6 +20,7 @@ evaluationDependsOn(':groovy-swing') dependencies { compile rootProject + compile project(':groovy-cli-commons') compile project(':groovy-swing') compile project(':groovy-templates') testCompile project(':groovy-test') http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-docgenerator/build.gradle ---------------------------------------------------------------------- diff --git a/subprojects/groovy-docgenerator/build.gradle b/subprojects/groovy-docgenerator/build.gradle index d6c88c3..77d93f2 100644 --- a/subprojects/groovy-docgenerator/build.gradle +++ b/subprojects/groovy-docgenerator/build.gradle @@ -18,6 +18,7 @@ */ dependencies { compile rootProject + compile project(':groovy-cli-commons') compile project(':groovy-templates') testCompile project(':groovy-test') compile "com.thoughtworks.qdox:qdox:$qdoxVersion" http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-groovydoc/build.gradle ---------------------------------------------------------------------- diff --git a/subprojects/groovy-groovydoc/build.gradle b/subprojects/groovy-groovydoc/build.gradle index 67a7030..3e86d0b 100644 --- a/subprojects/groovy-groovydoc/build.gradle +++ b/subprojects/groovy-groovydoc/build.gradle @@ -19,10 +19,10 @@ dependencies { compile rootProject testCompile rootProject.sourceSets.test.runtimeClasspath + compile project(':groovy-cli-commons') compile project(':groovy-templates') runtime project(':groovy-dateutil') testCompile project(':groovy-test') - testCompile project(':groovy-ant') testCompile "org.apache.ant:ant-testutil:$antVersion" } http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-groovysh/build.gradle ---------------------------------------------------------------------- diff --git a/subprojects/groovy-groovysh/build.gradle b/subprojects/groovy-groovysh/build.gradle index bd2d968..e234781 100644 --- a/subprojects/groovy-groovysh/build.gradle +++ b/subprojects/groovy-groovysh/build.gradle @@ -18,6 +18,7 @@ */ dependencies { compile rootProject + compile project(':groovy-cli-commons') compile project(':groovy-console') testCompile project(':groovy-test') compile("jline:jline:$jlineVersion") { http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/subprojects/groovy-test/build.gradle ---------------------------------------------------------------------- diff --git a/subprojects/groovy-test/build.gradle b/subprojects/groovy-test/build.gradle index 8dfd6ed..9a68ebc 100644 --- a/subprojects/groovy-test/build.gradle +++ b/subprojects/groovy-test/build.gradle @@ -19,7 +19,10 @@ dependencies { compile rootProject compile 'junit:junit:4.12' - testRuntime project(':groovy-ant') + // groovy-ant needed for FileNameFinder used in AllTestSuite and JavadocAssertionTestSuite + testRuntime(project(':groovy-ant')) { + transitive = false + } } -apply from: "${rootProject.projectDir}/gradle/jacoco/jacocofix.gradle" \ No newline at end of file +apply from: "${rootProject.projectDir}/gradle/jacoco/jacocofix.gradle"