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 2019/07/07 11:00:01 UTC

[groovy] branch master updated: GROOVY-9165: Grape cannot pull in picocli (don't use stock CliBuilder in our own classes) (closes #962)

This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new bdab243  GROOVY-9165: Grape cannot pull in picocli (don't use stock CliBuilder in our own classes) (closes #962)
bdab243 is described below

commit bdab24389e153567382ac356dab9c997800aa80e
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Jul 7 18:56:19 2019 +1000

    GROOVY-9165: Grape cannot pull in picocli (don't use stock CliBuilder in our own classes) (closes #962)
---
 gradle/assemble.gradle                             |   6 +-
 gradle/docs.gradle                                 |   4 +-
 .../groovy/cli/internal/CliBuilderInternal.groovy  | 420 +++++++++++++++++++++
 .../groovy/cli/internal/OptionAccessor.groovy      | 149 ++++++++
 subprojects/groovy-cli-picocli/build.gradle        |   2 +-
 subprojects/groovy-console/build.gradle            |   1 -
 .../main/groovy/groovy/console/ui/Console.groovy   |   6 +-
 .../src/main/groovy/groovy/ui/Console.groovy       |   6 +-
 subprojects/groovy-docgenerator/build.gradle       |   1 -
 .../apache/groovy/docgenerator/DocGenerator.groovy |   4 +-
 subprojects/groovy-groovydoc/build.gradle          |   1 -
 .../codehaus/groovy/tools/groovydoc/Main.groovy    |   4 +-
 subprojects/groovy-groovysh/build.gradle           |   1 -
 .../groovy/org/apache/groovy/groovysh/Main.groovy  |   6 +-
 .../org/codehaus/groovy/tools/shell/Main.groovy    |   6 +-
 15 files changed, 592 insertions(+), 25 deletions(-)

diff --git a/gradle/assemble.gradle b/gradle/assemble.gradle
index a1063e4..164fe77 100644
--- a/gradle/assemble.gradle
+++ b/gradle/assemble.gradle
@@ -368,7 +368,8 @@ ext.distSpec = copySpec {
                 it.file.name.startsWith('openbeans-') ||
                         it.file.name.startsWith('asm-') ||
                         it.file.name.startsWith('antlr-') ||
-                        it.file.name.startsWith('antlr4-')
+                        it.file.name.startsWith('antlr4-') ||
+                        it.file.name.startsWith('picocli-')
             }
         }
         from('src/bin/groovy.icns')
@@ -382,7 +383,8 @@ ext.distSpec = copySpec {
                             it.file.name.startsWith('asm-') ||
                             it.file.name.startsWith('antlr-') ||
                             it.file.name.startsWith('antlr4-') ||
-                            it.file.name.startsWith('openbeans-')
+                            it.file.name.startsWith('openbeans-') ||
+                            it.file.name.startsWith('picocli-')
                 }
             }
         }
diff --git a/gradle/docs.gradle b/gradle/docs.gradle
index 8585a28..9b966a8 100644
--- a/gradle/docs.gradle
+++ b/gradle/docs.gradle
@@ -126,12 +126,12 @@ task docProjectVersionInfo(type: Copy) {
 
 task docGDK {
     outputs.cacheIf { true }
-    dependsOn([project(':groovy-groovydoc'), project(':groovy-docgenerator'), project(':groovy-cli-picocli')]*.classes)
+    dependsOn([project(':groovy-groovydoc'), project(':groovy-docgenerator')]*.classes)
     dependsOn docProjectVersionInfo
     ext.destinationDir = "$buildDir/html/groovy-jdk"
     inputs.files sourceSets.main.runtimeClasspath + configurations.tools + files(docProjectVersionInfo.destinationDir)
     outputs.dir destinationDir
-    def docGeneratorPath = files(project(':groovy-docgenerator').sourceSets.main.output.classesDirs) + files(project(':groovy-cli-picocli').sourceSets.main.output.classesDirs)
+    def docGeneratorPath = files(project(':groovy-docgenerator').sourceSets.main.output.classesDirs)
     doLast { task ->
         try {
             ant {
diff --git a/src/main/groovy/groovy/cli/internal/CliBuilderInternal.groovy b/src/main/groovy/groovy/cli/internal/CliBuilderInternal.groovy
new file mode 100644
index 0000000..a3bc3ee
--- /dev/null
+++ b/src/main/groovy/groovy/cli/internal/CliBuilderInternal.groovy
@@ -0,0 +1,420 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.cli.internal
+
+import groovy.cli.CliBuilderException
+import groovy.cli.TypedOption
+import org.codehaus.groovy.runtime.InvokerHelper
+import picocli.CommandLine
+
+/**
+ * Cut-down version of CliBuilder with just enough functionality for Groovy's internal usage.
+ * Uses the embedded version of picocli classes.
+ * TODO: prune this right back to have only the functionality needed by Groovy commandline tools
+ */
+class CliBuilderInternal {
+    /**
+     * The command synopsis displayed as the first line in the usage help message, e.g., when <code>cli.usage()</code> is called.
+     * When not set, a default synopsis is generated that shows the supported options and parameters.
+     * @see #name
+     */
+    String usage = 'groovy'
+
+    /**
+     * This property allows customizing the program name displayed in the synopsis when <code>cli.usage()</code> is called.
+     * Ignored if the {@link #usage} property is set.
+     * @since 2.5
+     */
+    String name = 'groovy'
+
+    /**
+     * To disallow clustered POSIX short options, set this to false.
+     */
+    Boolean posix = true
+
+    /**
+     * 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
+
+    /**
+     * Configures what the parser should do when arguments not recognized
+     * as options are encountered: when <code>true</code> (the default), the
+     * remaining arguments are all treated as positional parameters.
+     * When <code>false</code>, the parser will continue to look for options, and
+     * only the unrecognized arguments are treated as positional parameters.
+     */
+    boolean stopAtNonOption = true
+
+    /**
+     * For backwards compatibility with Apache Commons CLI, set this property to
+     * <code>true</code> if the parser should recognize long options with both
+     * a single hyphen and a double hyphen prefix. The default is <code>false</code>,
+     * so only long options with a double hypen prefix (<code>--option</code>) are recognized.
+     * @since 2.5
+     */
+    boolean acceptLongOptionsWithSingleHyphen = false
+
+    /**
+     * The PrintWriter to write the {@link #usage} help message to
+     * when <code>cli.usage()</code> is called.
+     * Defaults to stdout but you can provide your own PrintWriter if desired.
+     */
+    PrintWriter writer = new PrintWriter(System.out)
+
+    /**
+     * The PrintWriter to write to when invalid user input was provided to
+     * the {@link #parse(java.lang.String[])} method.
+     * Defaults to stderr but you can provide your own PrintWriter if desired.
+     * @since 2.5
+     */
+    PrintWriter errorWriter = new PrintWriter(System.err)
+
+    /**
+     * Optional additional message for usage; displayed after the usage summary
+     * but before the options are displayed.
+     */
+    String header = null
+
+    /**
+     * Optional additional message for usage; displayed after the options.
+     */
+    String footer = null
+
+    /**
+     * Allows customisation of the usage message width.
+     */
+    int width = CommandLine.Model.UsageMessageSpec.DEFAULT_USAGE_WIDTH
+
+    /**
+     * Not normally accessed directly but allows fine-grained control over the
+     * parser behaviour via the API of the underlying library if needed.
+     * @since 2.5
+     */
+    // Implementation note: this object is separate from the CommandSpec.
+    // The values collected here are copied into the ParserSpec of the command.
+    final CommandLine.Model.ParserSpec parser = new CommandLine.Model.ParserSpec()
+            .stopAtPositional(true)
+            .unmatchedOptionsArePositionalParams(true)
+            .aritySatisfiedByAttachedOptionParam(true)
+            .limitSplit(true)
+            .overwrittenOptionsAllowed(true)
+            .toggleBooleanFlags(false)
+
+    /**
+     * Not normally accessed directly but allows fine-grained control over the
+     * usage help message via the API of the underlying library if needed.
+     * @since 2.5
+     */
+    // Implementation note: this object is separate from the CommandSpec.
+    // The values collected here are copied into the UsageMessageSpec of the command.
+    final CommandLine.Model.UsageMessageSpec usageMessage = new CommandLine.Model.UsageMessageSpec()
+
+    /**
+     * Internal data structure mapping option names to their associated {@link groovy.cli.TypedOption} object.
+     */
+    Map<String, TypedOption> savedTypeOptions = new HashMap<String, TypedOption>()
+
+    // CommandSpec is the entry point into the picocli object model for a command.
+    // It gives access to a ParserSpec to customize the parser behaviour and
+    // a UsageMessageSpec to customize the usage help message.
+    // Add OptionSpec and PositionalParamSpec objects to this object to define
+    // the options and positional parameters this command recognizes.
+    //
+    // This field is private for now.
+    // It is initialized to an empty spec so options and positional parameter specs
+    // can be added dynamically via the programmatic API.
+    // When a command spec is defined via annotations, the existing instance is
+    // replaced with a new one. This allows the outer CliBuilder instance can be reused.
+    private CommandLine.Model.CommandSpec commandSpec = CommandLine.Model.CommandSpec.create()
+
+    /**
+     * Sets the {@link #usage usage} property on this <code>CliBuilder</code> and the
+     * <code>customSynopsis</code> on the {@link #usageMessage} used by the underlying library.
+     * @param usage the custom synopsis of the usage help message
+     */
+    void setUsage(String usage) {
+        this.usage = usage
+        usageMessage.customSynopsis(usage)
+    }
+
+    /**
+     * Sets the {@link #footer} property on this <code>CliBuilder</code>
+     * and on the {@link #usageMessage} used by the underlying library.
+     * @param footer the footer of the usage help message
+     */
+    void setFooter(String footer) {
+        this.footer = footer
+        usageMessage.footer(footer)
+    }
+
+    /**
+     * Sets the {@link #header} property on this <code>CliBuilder</code> and the
+     * <code>description</code> on the {@link #usageMessage} used by the underlying library.
+     * @param header the description text of the usage help message
+     */
+    void setHeader(String header) {
+        this.header = header
+        // "header" is displayed after the synopsis in previous CliBuilder versions.
+        // The picocli equivalent is the "description".
+        usageMessage.description(header)
+    }
+
+    /**
+     * Sets the {@link #width} property on this <code>CliBuilder</code>
+     * and on the {@link #usageMessage} used by the underlying library.
+     * @param width the width of the usage help message
+     */
+    void setWidth(int width) {
+        this.width = width
+        usageMessage.width(width)
+    }
+
+    /**
+     * Sets the {@link #expandArgumentFiles} property on this <code>CliBuilder</code>
+     * and on the {@link #parser} used by the underlying library.
+     * @param expand whether to expand argument @-files
+     */
+    void setExpandArgumentFiles(boolean expand) {
+        this.expandArgumentFiles = expand
+        parser.expandAtFiles(expand)
+    }
+
+    /**
+     * Sets the {@link #posix} property on this <code>CliBuilder</code> and the
+     * <code>posixClusteredShortOptionsAllowed</code> property on the {@link #parser}
+     * used by the underlying library.
+     * @param posix whether to allow clustered short options
+     */
+    void setPosix(Boolean posix) {
+        this.posix = posix
+        parser.posixClusteredShortOptionsAllowed(posix ?: false)
+    }
+
+    /**
+     * Sets the {@link #stopAtNonOption} property on this <code>CliBuilder</code> and the
+     * <code>stopAtPositional</code> property on the {@link #parser}
+     * used by the underlying library.
+     * @param stopAtNonOption when <code>true</code> (the default), the
+     *          remaining arguments are all treated as positional parameters.
+     *          When <code>false</code>, the parser will continue to look for options, and
+     *          only the unrecognized arguments are treated as positional parameters.
+     */
+    void setStopAtNonOption(boolean stopAtNonOption) {
+        this.stopAtNonOption = stopAtNonOption
+        parser.stopAtPositional(stopAtNonOption)
+        parser.unmatchedOptionsArePositionalParams(stopAtNonOption)
+    }
+
+    /**
+     * For backwards compatibility reasons, if a custom {@code writer} is set, this sets
+     * both the {@link #writer} and the {@link #errorWriter} to the specified writer.
+     * @param writer the writer to initialize both the {@code writer} and the {@code errorWriter} to
+     */
+    void setWriter(PrintWriter writer) {
+        this.writer = writer
+        this.errorWriter = writer
+    }
+
+    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]) // args[0] is description
+                commandSpec.addOption(option)
+                return create(option, null, null, null)
+            }
+            if (args.size() == 1 && args[0] instanceof CommandLine.Model.OptionSpec && name == 'leftShift') {
+                CommandLine.Model.OptionSpec option = args[0] as CommandLine.Model.OptionSpec
+                commandSpec.addOption(option)
+                return create(option, null, null, null)
+            }
+            if (args.size() == 2 && args[0] instanceof Map) {
+                Map m = args[0] as Map
+                if (m.type && !(m.type instanceof Class)) {
+                    throw new CliBuilderException("'type' must be a Class")
+                }
+                def option = option(name, m, args[1])
+                commandSpec.addOption(option)
+                return create(option, m.type, option.defaultValue(), option.converters())
+            }
+        }
+        return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args)
+    }
+
+    private TypedOption create(CommandLine.Model.OptionSpec o, Class theType, defaultValue, convert) {
+        String opt = o.names().sort { a, b -> a.length() - b.length() }.first()
+        opt = opt?.length() == 2 ? opt.substring(1) : null
+
+        String longOpt = o.names().sort { a, b -> b.length() - a.length() }.first()
+        longOpt = longOpt?.startsWith("--") ? longOpt.substring(2) : null
+
+        Map<String, Object> result = new TypedOption<Object>()
+        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[longOpt ?: opt] = result
+        result
+    }
+
+    /**
+     * Make options accessible from command line args with parser.
+     * Returns null on bad command lines after displaying usage message.
+     */
+    OptionAccessor parse(args) {
+        CommandLine commandLine = createCommandLine()
+        try {
+            def accessor = new OptionAccessor(commandLine.parseArgs(args as String[]))
+            accessor.savedTypeOptions = savedTypeOptions
+            return accessor
+        } catch (CommandLine.ParameterException pe) {
+            errorWriter.println("error: " + pe.message)
+            printUsage(pe.commandLine, errorWriter)
+            return null
+        }
+    }
+
+    private CommandLine createCommandLine() {
+        commandSpec.parser(parser)
+        commandSpec.name(name).usageMessage(usageMessage)
+        if (commandSpec.positionalParameters().empty) {
+            commandSpec.addPositional(CommandLine.Model.PositionalParamSpec.builder().type(String[]).arity("*").paramLabel("P").hidden(true).build())
+        }
+        return new CommandLine(commandSpec)
+    }
+
+    /**
+     * Prints the usage message with the specified {@link #header header}, {@link #footer footer} and {@link #width width}
+     * to the specified {@link #writer writer} (default: System.out).
+     */
+    void usage() {
+        printUsage(commandSpec.commandLine() ?: createCommandLine(), writer)
+    }
+
+    private void printUsage(CommandLine commandLine, PrintWriter pw) {
+        commandLine.usage(pw)
+        pw.flush()
+    }
+
+    private static class ArgSpecAttributes {
+        Class type
+        Class[] auxiliaryTypes
+        String label
+        CommandLine.Model.IGetter getter
+        CommandLine.Model.ISetter setter
+        Object initialValue
+        boolean hasInitialValue
+    }
+
+    // implementation details -------------------------------------
+    /**
+     * Internal method: How to create an OptionSpec from the specification.
+     */
+    CommandLine.Model.OptionSpec option(shortname, Map details, description) {
+        CommandLine.Model.OptionSpec.Builder builder
+        if (shortname == '_') {
+            builder = CommandLine.Model.OptionSpec.builder("--$details.longOpt").description(description)
+            if (acceptLongOptionsWithSingleHyphen) {
+                builder.names("-$details.longOpt", "--$details.longOpt")
+            }
+            details.remove('longOpt')
+        } else {
+            builder = CommandLine.Model.OptionSpec.builder("-$shortname").description(description)
+        }
+        commons2picocli(shortname, details).each { key, value ->
+            if (builder.hasProperty(key)) {
+                builder[key] = value
+            } else if (key != 'opt') {    // GROOVY-8607 ignore opt since we already have that
+                builder.invokeMethod(key, value)
+            }
+        }
+        if (!builder.type() && !builder.arity() && builder.converters()?.length > 0) {
+            builder.arity("1").type(details.convert ? Object : String[])
+        }
+        return builder.build()
+    }
+
+    /** Commons-cli constant that specifies the number of argument values is infinite */
+    private static final int COMMONS_CLI_UNLIMITED_VALUES = -2
+
+    // - argName:        String
+    // - longOpt:        String
+    // - args:           int or String
+    // - optionalArg:    boolean
+    // - required:       boolean
+    // - type:           Class
+    // - valueSeparator: char
+    // - convert:        Closure
+    // - defaultValue:   String
+    private Map commons2picocli(shortname, Map m) {
+        if (m.args && m.optionalArg) {
+            m.arity = "0..${m.args}"
+            m.remove('args')
+            m.remove('optionalArg')
+        }
+        if (!m.defaultValue) {
+            m.remove('defaultValue') // don't default the picocli model to empty string
+        }
+        def result = m.collectMany { k, v ->
+            if (k == 'args' && v == '+') {
+                [[arity: '1..*']]
+            } else if (k == 'args' && v == 0) {
+                [[arity: '0']]
+            } else if (k == 'args') {
+                v == COMMONS_CLI_UNLIMITED_VALUES ? [[arity: "*"]] : [[arity: "$v"]]
+            } else if (k == 'optionalArg') {
+                v ? [[arity: '0..1']] : [[arity: '1']]
+            } else if (k == 'argName') {
+                [[paramLabel: "<$v>"]]
+            } else if (k == 'longOpt') {
+                acceptLongOptionsWithSingleHyphen ?
+                        [[names: ["-$shortname", "-$v", "--$v"] as String[] ]] :
+                        [[names: ["-$shortname",        "--$v"] as String[] ]]
+            } else if (k == 'valueSeparator') {
+                [[splitRegex: "$v"]]
+            } else if (k == 'convert') {
+                [[converters: [v] as CommandLine.ITypeConverter[] ]]
+            } else {
+                [[(k): v]]
+            }
+        }.sum() as Map
+        result
+    }
+}
diff --git a/src/main/groovy/groovy/cli/internal/OptionAccessor.groovy b/src/main/groovy/groovy/cli/internal/OptionAccessor.groovy
new file mode 100644
index 0000000..af6105a
--- /dev/null
+++ b/src/main/groovy/groovy/cli/internal/OptionAccessor.groovy
@@ -0,0 +1,149 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.cli.internal
+
+import groovy.cli.TypedOption
+import org.codehaus.groovy.runtime.InvokerHelper
+import org.codehaus.groovy.runtime.StringGroovyMethods
+import picocli.CommandLine.Model.OptionSpec
+import picocli.CommandLine.ParseResult
+
+class OptionAccessor {
+    ParseResult parseResult
+    Map<String, TypedOption> savedTypeOptions
+
+    OptionAccessor(ParseResult parseResult) {
+        this.parseResult = parseResult
+    }
+
+    boolean hasOption(TypedOption typedOption) {
+        parseResult.hasMatchedOption(typedOption.longOpt ?: typedOption.opt as String)
+    }
+
+    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 (parseResult.hasMatchedOption(optionName)) {
+            return parseResult.matchedOptionValue(optionName, defaultValue)
+        } else {
+            OptionSpec option = parseResult.commandSpec().findOption(optionName)
+            return option ? option.value : defaultValue
+        }
+    }
+
+    public <T> T getAt(TypedOption<T> typedOption) {
+        getAt(typedOption, null)
+    }
+
+    public <T> T getAt(TypedOption<T> typedOption, T defaultValue) {
+        getOptionValue(typedOption, defaultValue)
+    }
+
+    private <T> T getTypedValue(Class<T> type, String optionName, String optionValue) {
+        if (savedTypeOptions[optionName]?.cliOption?.arity?.min == 0) { // TODO is this not a bug?
+            return (T) parseResult.hasMatchedOption(optionName) // TODO should defaultValue not simply convert the type regardless of the matched value?
+        }
+        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 == Boolean || type == Boolean.TYPE) {
+            return type.cast(Boolean.parseBoolean(optionValue))
+        }
+        StringGroovyMethods.asType(optionValue, (Class<T>) type)
+    }
+
+    Properties getOptionProperties(String name) {
+        if (!parseResult.hasMatchedOption(name)) {
+            return null
+        }
+        List<String> keyValues = parseResult.matchedOption(name).stringValues()
+        Properties result = new Properties()
+        keyValues.toSpreadMap().each { k, v -> result.setProperty(k, v) }
+        result
+    }
+
+    def invokeMethod(String name, Object args) {
+        // TODO we could just declare normal methods to map commons-cli CommandLine methods to picocli ParseResult methods
+        if (name == 'hasOption')      { name = 'hasMatchedOption';   args = [args[0]      ].toArray() }
+        if (name == 'getOptionValue') { name = 'matchedOptionValue'; args = [args[0], null].toArray() }
+        return InvokerHelper.getMetaClass(parseResult).invokeMethod(parseResult, name, args)
+    }
+
+    def getProperty(String name) {
+        if (name == 'parseResult') { return parseResult }
+        if (parseResult.hasMatchedOption(name)) {
+            def result = parseResult.matchedOptionValue(name, null)
+
+            // if user specified an array type, return the full array (regardless of 's' suffix on name)
+            Class userSpecifiedType = savedTypeOptions[name]?.type
+            if (userSpecifiedType?.isArray()) { return result }
+
+            // otherwise, if the result is multi-value, return the first value
+            Class derivedType = parseResult.matchedOption(name).type()
+            if (derivedType.isArray()) {
+                return result ? result[0] : null
+            } else if (Collection.class.isAssignableFrom(derivedType)) {
+                return (result as Collection)?.first()
+            }
+            if (!userSpecifiedType && result == '' && parseResult.matchedOption(name).arity().min == 0) {
+                return true
+            }
+            return parseResult.matchedOption(name).typedValues().get(0)
+        }
+        if (parseResult.commandSpec().findOption(name)) { // requested option was not matched: return its default
+            def option = parseResult.commandSpec().findOption(name)
+            def result = option.value
+            return result ? result : false
+        }
+        if (name.size() > 1 && name.endsWith('s')) { // user wants multi-value result
+            def singularName = name[0..-2]
+            if (parseResult.hasMatchedOption(singularName)) {
+                // if picocli has a strongly typed multi-value result, return it
+                Class type = parseResult.matchedOption(singularName).type()
+                if (type.isArray() || Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)) {
+                    return parseResult.matchedOptionValue(singularName, null)
+                }
+                // otherwise, return the raw string values as a list
+                return parseResult.matchedOption(singularName).stringValues()
+            }
+        }
+        false
+    }
+
+    List<String> arguments() {
+        parseResult.hasMatchedPositional(0) ? parseResult.matchedPositional(0).stringValues() : []
+    }
+}
diff --git a/subprojects/groovy-cli-picocli/build.gradle b/subprojects/groovy-cli-picocli/build.gradle
index d83ddfd..87f0177 100644
--- a/subprojects/groovy-cli-picocli/build.gradle
+++ b/subprojects/groovy-cli-picocli/build.gradle
@@ -18,7 +18,7 @@
  */
 dependencies {
     compile rootProject
-    compile "info.picocli:picocli:$picocliVersion"
+    provided "info.picocli:picocli:$picocliVersion"
     testCompile rootProject.sourceSets.test.output
     testCompile project(':groovy-test')
     testCompile project(':groovy-dateutil')
diff --git a/subprojects/groovy-console/build.gradle b/subprojects/groovy-console/build.gradle
index 44f9bdb..a8ace80 100644
--- a/subprojects/groovy-console/build.gradle
+++ b/subprojects/groovy-console/build.gradle
@@ -20,7 +20,6 @@ evaluationDependsOn(':groovy-swing')
 
 dependencies {
     compile rootProject
-    compile project(':groovy-cli-picocli')
     compile project(':groovy-swing')
     compile project(':groovy-templates')
     testCompile project(':groovy-test')
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
index 7c82101..f194342 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/console/ui/Console.groovy
@@ -18,8 +18,8 @@
  */
 package groovy.console.ui
 
-import groovy.cli.picocli.CliBuilder
-import groovy.cli.picocli.OptionAccessor
+import groovy.cli.internal.CliBuilderInternal
+import groovy.cli.internal.OptionAccessor
 import groovy.console.ui.text.FindReplaceUtility
 import groovy.console.ui.text.GroovyFilter
 import groovy.console.ui.text.SmartDocumentFilter
@@ -235,7 +235,7 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
 
     static void main(args) {
         MessageSource messages = new MessageSource(Console)
-        CliBuilder cli = new CliBuilder(usage: 'groovyConsole [options] [filename]', stopAtNonOption: false,
+        def cli = new CliBuilderInternal(usage: 'groovyConsole [options] [filename]', stopAtNonOption: false,
                 header: messages['cli.option.header'])
         cli.with {
             _(names: ['-cp', '-classpath', '--classpath'], messages['cli.option.classpath.description'])
diff --git a/subprojects/groovy-console/src/main/groovy/groovy/ui/Console.groovy b/subprojects/groovy-console/src/main/groovy/groovy/ui/Console.groovy
index 52d83e6..181ef6a 100644
--- a/subprojects/groovy-console/src/main/groovy/groovy/ui/Console.groovy
+++ b/subprojects/groovy-console/src/main/groovy/groovy/ui/Console.groovy
@@ -18,8 +18,8 @@
  */
 package groovy.ui
 
-import groovy.cli.picocli.CliBuilder
-import groovy.cli.picocli.OptionAccessor
+import groovy.cli.internal.CliBuilderInternal
+import groovy.cli.internal.OptionAccessor
 import groovy.inspect.swingui.AstBrowser
 import groovy.inspect.swingui.ObjectBrowser
 import groovy.swing.SwingBuilder
@@ -229,7 +229,7 @@ class Console implements CaretListener, HyperlinkListener, ComponentListener, Fo
 
     static void main(args) {
         MessageSource messages = new MessageSource(Console)
-        CliBuilder cli = new CliBuilder(usage: 'groovyConsole [options] [filename]', stopAtNonOption: false,
+        def cli = new CliBuilderInternal(usage: 'groovyConsole [options] [filename]', stopAtNonOption: false,
                 header: messages['cli.option.header'])
         cli.with {
             _(names: ['-cp', '-classpath', '--classpath'], messages['cli.option.classpath.description'])
diff --git a/subprojects/groovy-docgenerator/build.gradle b/subprojects/groovy-docgenerator/build.gradle
index 0d270e4..3106ed0 100644
--- a/subprojects/groovy-docgenerator/build.gradle
+++ b/subprojects/groovy-docgenerator/build.gradle
@@ -18,7 +18,6 @@
  */
 dependencies {
     compile rootProject
-    compile project(':groovy-cli-picocli')
     compile project(':groovy-templates')
     testCompile project(':groovy-test')
     compile "com.thoughtworks.qdox:qdox:$qdoxVersion"
diff --git a/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/DocGenerator.groovy b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/DocGenerator.groovy
index da325d7..b02f60c 100644
--- a/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/DocGenerator.groovy
+++ b/subprojects/groovy-docgenerator/src/main/groovy/org/apache/groovy/docgenerator/DocGenerator.groovy
@@ -23,7 +23,7 @@ import com.thoughtworks.qdox.model.JavaClass
 import com.thoughtworks.qdox.model.JavaMethod
 import com.thoughtworks.qdox.model.JavaParameter
 import com.thoughtworks.qdox.model.Type
-import groovy.cli.picocli.CliBuilder
+import groovy.cli.internal.CliBuilderInternal
 import groovy.text.SimpleTemplateEngine
 import groovy.text.Template
 import groovy.text.TemplateEngine
@@ -207,7 +207,7 @@ class DocGenerator {
      * Main entry point.
      */
     static void main(String... args) {
-        def cli = new CliBuilder(usage : 'DocGenerator [options] [sourcefiles]', posix:false)
+        def cli = new CliBuilderInternal(usage : 'DocGenerator [options] [sourcefiles]', posix:false)
         cli.help(longOpt: 'help', messages['cli.option.help.description'])
         cli._(longOpt: 'version', messages['cli.option.version.description'])
         cli.o(longOpt: 'outputDir', args:1, argName: 'path', messages['cli.option.output.dir.description'])
diff --git a/subprojects/groovy-groovydoc/build.gradle b/subprojects/groovy-groovydoc/build.gradle
index 80696b1..77938d4 100644
--- a/subprojects/groovy-groovydoc/build.gradle
+++ b/subprojects/groovy-groovydoc/build.gradle
@@ -19,7 +19,6 @@
 dependencies {
     compile rootProject
     testCompile rootProject.sourceSets.test.runtimeClasspath
-    compile project(':groovy-cli-picocli')
     compile project(':groovy-templates')
     runtime project(':groovy-docgenerator')
     testCompile project(':groovy-test')
diff --git a/subprojects/groovy-groovydoc/src/main/groovy/org/codehaus/groovy/tools/groovydoc/Main.groovy b/subprojects/groovy-groovydoc/src/main/groovy/org/codehaus/groovy/tools/groovydoc/Main.groovy
index 4646dec..5a5bcb8 100644
--- a/subprojects/groovy-groovydoc/src/main/groovy/org/codehaus/groovy/tools/groovydoc/Main.groovy
+++ b/subprojects/groovy-groovydoc/src/main/groovy/org/codehaus/groovy/tools/groovydoc/Main.groovy
@@ -18,7 +18,7 @@
  */
 package org.codehaus.groovy.tools.groovydoc
 
-import groovy.cli.picocli.CliBuilder
+import groovy.cli.internal.CliBuilderInternal
 import groovy.io.FileType
 import org.codehaus.groovy.tools.groovydoc.gstringTemplates.GroovyDocTemplateInfo
 import org.codehaus.groovy.tools.shell.IO
@@ -58,7 +58,7 @@ class Main {
         IO io = new IO()
         Logger.io = io
 
-        def cli = new CliBuilder(usage: 'groovydoc [options] [packagenames] [sourcefiles]', writer: io.out, posix: false,
+        def cli = new CliBuilderInternal(usage: 'groovydoc [options] [packagenames] [sourcefiles]', writer: io.out, posix: false,
                 header: messages['cli.option.header'])
 
         cli._(names: ['-h', '-help', '--help'], messages['cli.option.help.description'])
diff --git a/subprojects/groovy-groovysh/build.gradle b/subprojects/groovy-groovysh/build.gradle
index 2b7dfbb..7cec73f 100644
--- a/subprojects/groovy-groovysh/build.gradle
+++ b/subprojects/groovy-groovysh/build.gradle
@@ -18,7 +18,6 @@
  */
 dependencies {
     compile rootProject
-    compile project(':groovy-cli-picocli')
     compile project(':groovy-console')
     testCompile project(':groovy-test')
     compile("jline:jline:$jlineVersion") {
diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
index fc3d5b8..a36ffce 100644
--- a/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
+++ b/subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/Main.groovy
@@ -18,8 +18,8 @@
  */
 package org.apache.groovy.groovysh
 
-import groovy.cli.picocli.CliBuilder
-import groovy.cli.picocli.OptionAccessor
+import groovy.cli.internal.CliBuilderInternal
+import groovy.cli.internal.OptionAccessor
 import jline.TerminalFactory
 import jline.UnixTerminal
 import jline.UnsupportedTerminal
@@ -72,7 +72,7 @@ class Main {
      */
     static void main(final String[] args) {
         MessageSource messages = new MessageSource(Main)
-        CliBuilder cli = new CliBuilder(usage: 'groovysh [options] [...]', stopAtNonOption: false,
+        def cli = new CliBuilderInternal(usage: 'groovysh [options] [...]', stopAtNonOption: false,
                 header: messages['cli.option.header'])
         cli.with {
             _(names: ['-cp', '-classpath', '--classpath'], messages['cli.option.classpath.description'])
diff --git a/subprojects/groovy-groovysh/src/main/groovy/org/codehaus/groovy/tools/shell/Main.groovy b/subprojects/groovy-groovysh/src/main/groovy/org/codehaus/groovy/tools/shell/Main.groovy
index 8017171..6d8bd81 100644
--- a/subprojects/groovy-groovysh/src/main/groovy/org/codehaus/groovy/tools/shell/Main.groovy
+++ b/subprojects/groovy-groovysh/src/main/groovy/org/codehaus/groovy/tools/shell/Main.groovy
@@ -18,8 +18,8 @@
  */
 package org.codehaus.groovy.tools.shell
 
-import groovy.cli.picocli.CliBuilder
-import groovy.cli.picocli.OptionAccessor
+import groovy.cli.internal.CliBuilderInternal
+import groovy.cli.internal.OptionAccessor
 import jline.TerminalFactory
 import jline.UnixTerminal
 import jline.UnsupportedTerminal
@@ -72,7 +72,7 @@ class Main {
      */
     static void main(final String[] args) {
         MessageSource messages = new MessageSource(Main)
-        CliBuilder cli = new CliBuilder(usage: 'groovysh [options] [...]', stopAtNonOption: false,
+        def cli = new CliBuilderInternal(usage: 'groovysh [options] [...]', stopAtNonOption: false,
                 header: messages['cli.option.header'])
         cli.with {
             _(names: ['-cp', '-classpath', '--classpath'], messages['cli.option.classpath.description'])