You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2018/04/15 08:14:19 UTC
[1/2] groovy git commit: move commons-cli CliBuilder into its own
subproject
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"
[2/2] groovy git commit: move commons-cli CliBuilder into its own
subproject
Posted by pa...@apache.org.
move commons-cli CliBuilder into its own subproject
Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/14498365
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/14498365
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/14498365
Branch: refs/heads/GROOVY_2_5_X
Commit: 144983659bebe0bac99fb9af73a3f77f5e6308ae
Parents: 1a13cf6
Author: Paul King <pa...@asert.com.au>
Authored: Sat Apr 14 11:05:35 2018 +1000
Committer: Paul King <pa...@asert.com.au>
Committed: Sun Apr 15 18:13:28 2018 +1000
----------------------------------------------------------------------
gradle/binarycompatibility.gradle | 2 +-
settings.gradle | 1 +
src/main/groovy/groovy/util/CliBuilder.groovy | 798 -------------------
src/test/groovy/util/CliBuilderTest.groovy | 707 ----------------
subprojects/groovy-cli-commons/build.gradle | 24 +
.../main/groovy/groovy/util/CliBuilder.groovy | 798 +++++++++++++++++++
.../groovy/groovy/util/CliBuilderTest.groovy | 707 ++++++++++++++++
subprojects/groovy-console/build.gradle | 1 +
subprojects/groovy-docgenerator/build.gradle | 1 +
subprojects/groovy-groovydoc/build.gradle | 2 +-
subprojects/groovy-groovysh/build.gradle | 1 +
subprojects/groovy-test/build.gradle | 7 +-
12 files changed, 1540 insertions(+), 1509 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/gradle/binarycompatibility.gradle
----------------------------------------------------------------------
diff --git a/gradle/binarycompatibility.gradle b/gradle/binarycompatibility.gradle
index c7e26f2..998e26b 100644
--- a/gradle/binarycompatibility.gradle
+++ b/gradle/binarycompatibility.gradle
@@ -35,7 +35,7 @@ task checkBinaryCompatibility {
check.dependsOn(checkBinaryCompatibility)
// for comparing between versions with different modules, set excludeModules to differing modules, e.g.
-def excludeModules = ['groovy-dateutil', 'groovy-datetime', 'performance', 'groovy-macro', 'tests-vm8']
+def excludeModules = ['groovy-cli-commons', 'groovy-dateutil', 'groovy-datetime', 'performance', 'groovy-macro', 'tests-vm8']
//def excludeModules = []
Set projectsToCheck = allprojects.findAll{ !(it.name in excludeModules) }
http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/settings.gradle
----------------------------------------------------------------------
diff --git a/settings.gradle b/settings.gradle
index 0ada19f..5145623 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -18,6 +18,7 @@
*/
def subprojects = ['groovy-ant',
'groovy-bsf',
+ 'groovy-cli-commons',
'groovy-console',
'groovy-dateutil',
'groovy-docgenerator',
http://git-wip-us.apache.org/repos/asf/groovy/blob/14498365/src/main/groovy/groovy/util/CliBuilder.groovy
----------------------------------------------------------------------
diff --git a/src/main/groovy/groovy/util/CliBuilder.groovy b/src/main/groovy/groovy/util/CliBuilder.groovy
deleted file mode 100644
index bc7d44a..0000000
--- a/src/main/groovy/groovy/util/CliBuilder.groovy
+++ /dev/null
@@ -1,798 +0,0 @@
-/*
- * 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/src/test/groovy/util/CliBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/groovy/util/CliBuilderTest.groovy b/src/test/groovy/util/CliBuilderTest.groovy
deleted file mode 100644
index 175228b..0000000
--- a/src/test/groovy/util/CliBuilderTest.groovy
+++ /dev/null
@@ -1,707 +0,0 @@
-/*
- * 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, 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
- }
- }
- 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-cli-commons/build.gradle
----------------------------------------------------------------------
diff --git a/subprojects/groovy-cli-commons/build.gradle b/subprojects/groovy-cli-commons/build.gradle
new file mode 100644
index 0000000..c77cf10
--- /dev/null
+++ b/subprojects/groovy-cli-commons/build.gradle
@@ -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.
+ */
+dependencies {
+ compile rootProject
+ compile "commons-cli:commons-cli:$commonsCliVersion"
+ testCompile project(':groovy-test')
+ testCompile project(':groovy-dateutil')
+}