Skip to content

Commit

Permalink
GROOVY-8520 add picocli-based CliBuilder (closes #688)
Browse files Browse the repository at this point in the history
  • Loading branch information
remkop authored and paulk-asert committed Apr 27, 2018
1 parent 3e2035a commit 237f8cc
Show file tree
Hide file tree
Showing 11 changed files with 2,621 additions and 9 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ ext {
luceneVersion = '4.7.2'
openbeansVersion = '1.0'
openejbVersion = '1.0'
picocliVersion = '3.0.0-beta-2'
qdoxVersion = '1.12.1'
slf4jVersion = '1.7.21'
xmlunitVersion = '1.6'
Expand All @@ -181,6 +182,7 @@ dependencies {
compile "org.ow2.asm:asm-util:$asmVersion"

compile "commons-cli:commons-cli:$commonsCliVersion"
compile "info.picocli:picocli:$picocliVersion"
compile "org.apache.ant:ant:$antVersion"
compile("com.thoughtworks.xstream:xstream:$xstreamVersion") {
exclude(group: 'xpp3', module: 'xpp3_min')
Expand Down
2 changes: 1 addition & 1 deletion gradle/binarycompatibility.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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-cli-commons', 'groovy-dateutil', 'groovy-datetime', 'performance', 'groovy-macro', 'tests-vm8', 'groovy-json-direct']
def excludeModules = ['groovy-cli-picocli', 'groovy-cli-commons', 'groovy-dateutil', 'groovy-datetime', 'performance', 'groovy-macro', 'tests-vm8', 'groovy-json-direct']
//def excludeModules = []

Set projectsToCheck = allprojects.findAll{ !(it.name in excludeModules) }
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
def subprojects = ['groovy-ant',
'groovy-bsf',
'groovy-cli-commons',
'groovy-cli-picocli',
'groovy-console',
'groovy-datetime',
'groovy-dateutil',
Expand Down
Binary file added src/spec/assets/img/usageMessageSpec.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 109 additions & 8 deletions src/spec/doc/core-domain-specific-languages.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,7 @@ properties are supported when specifying an allowed commandline option:
| type | the type of this option | `Class`
| valueSeparator | the character that is the value separator | `char`<2>
| defaultValue | a default value | `String`
| convert | converts the incoming String to the required type | `Closure`<2>
| convert | converts the incoming String to the required type | `Closure`<1>
|======================
<1> More details later
<2> Single character Strings are coerced to chars in special cases in Groovy
Expand Down Expand Up @@ -1324,7 +1324,7 @@ multiple arguments.
Arguments on the commandline are by nature Strings (or arguably can be considered Booleans for flags) but can be
converted to richer types automatically by supplying additional typing information. For the
annotation-based argument definition style, these types are supplied using the field types for annotation
properties or return types of annotated methods (are the setter argument type for setter methods).
properties or return types of annotated methods (or the setter argument type for setter methods).
For the dynamic method style of argument definition a special 'type' property is supported
which allows you to specify a Class name.
Expand Down Expand Up @@ -1367,14 +1367,15 @@ include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withConve
===== Options with multiple arguments
Multiple arguments are also supported using an args value greater than 1. There is a special named parameter,
Multiple arguments are also supported using an `args` value greater than 1. There is a special named parameter,
`valueSeparator`, which can also be optionally used when processing multiple arguments. It allows some additional
flexibility in the syntax supported when supplying such argument lists on the commandline. For example,
supplying a value separator of ',' allows a comma-delimited list of values to be passed on the commandline.
The `args` value is normally an integer. It can be optionally supplied as a String. There are two special
String symbols: `+` and `*`. The `*` value means 0 or more. The `+` value means 1 or more. The `*` value is
the same as using `+` and also setting the `optionalArg` value to true.
String symbols: `&plus;` and `&#42;`.
The `&#42;` value means 0 or more. The `&plus;` value means 1 or more.
The `&#42;` value is the same as using `&plus;` and also setting the `optionalArg` value to true.
Accessing the multiple arguments follows a special convention. Simply add an 's' to the normal property
you would use to access the argument option and you will retrieve all the supplied arguments as a list.
Expand Down Expand Up @@ -1495,8 +1496,8 @@ Then, the following statements can be in a separate part of your code which is t
----
def args = '--age 21'.split()
def options = cli.parse(args)
int age = options[age]
assert age == 21
int a = options[age]
assert a == 21
----
Finally, there is one additional convenience method offered by `CliBuilder` to even allow the
Expand All @@ -1516,7 +1517,8 @@ include::{projectdir}/src/spec/test/builder/CliBuilderTest.groovy[tags=withTypeC
===============================
*NOTE* Advanced CLI features
`CliBuilder` can be thought of as a Groovy friendly wrapper on top of (currently) Apache Commons CLI.
`CliBuilder` can be thought of as a Groovy friendly wrapper on top of either
https://github.com/remkop/picocli[picocli] or https://commons.apache.org/proper/commons-cli/[Apache Commons CLI].
If there is a feature not provided by `CliBuilder` that you know is supported in the underlying
library, the current `CliBuilder` implementation (and various Groovy language features) make it easy for you
to call the underlying library methods directly. Doing so is a pragmatic way to leverage the Groovy-friendly
Expand All @@ -1525,6 +1527,8 @@ A word of caution however; future versions of `CliBuilder` could potentially use
and in that event, some porting work may be required for your Groovy classes and/or scripts.
===============================
====== Apache Commons CLI
As an example, here is some code for making use of Apache Commons CLI's grouping mechanism:
[source,groovy]
Expand All @@ -1544,6 +1548,103 @@ assert !cli.parse('-d -o'.split()) // <1>
----
<1> The parse will fail since only one option from a group can be used at a time.
====== Picocli
Below are some features available in the picocli version of `CliBuilder`.
*New property: errorWriter*
When users of your application give invalid command line arguments,
CliBuilder writes an error message and the usage help message to the `stderr` output stream.
It doesn’t use the `stdout` stream to prevent the error message from being parsed when your program's
output is used as input for another process.
You can customize the destination by setting the `errorWriter` to a different value.
On the other hand, `CliBuilder.usage()` prints the usage help message to the `stdout` stream.
This way, when users request help (e.g. with a `--help` parameter),
they can pipe the output to a utility like `less` or `grep`.
You can specify different writers for testing.
_Be aware that for backwards compatibility, setting the `writer` property to a different value
will set *both* the `writer` and the `errorWriter` to the specified writer._
*ANSI colors*
The picocli version of CliBuilder renders the usage help message in ANSI colors on supported platforms automatically.
If desired you can http://picocli.info/#_usage_help_with_styles_and_colors[customize] this.
(An example follows below.)
*New property: name*
As before, you can set the synopsis of the usage help message with the `usage` property.
You may be interested in a small improvement:
if you only set the command `name`, a synopsis will be generated automatically,
with repeating elements followed by `...` and optional elements surrounded with `[` and `]`.
(An example follows below.)
*New property: usageMessage*
This property exposes a `UsageMessageSpec` object from the underlying picocli library,
which gives fine-grained control over various sections of the usage help message. For example:
[source,groovy]
----
def cli = new CliBuilder()
cli.name = "myapp"
cli.usageMessage.with {
headerHeading("@|bold,underline Header heading:|@%n")
header("Header 1", "Header 2") // before the synopsis
synopsisHeading("%n@|bold,underline Usage:|@ ")
descriptionHeading("%n@|bold,underline Description heading:|@%n")
description("Description 1", "Description 2") // after the synopsis
optionListHeading("%n@|bold,underline Options heading:|@%n")
footerHeading("%n@|bold,underline Footer heading:|@%n")
footer("Footer 1", "Footer 2")
}
cli.a('option a description')
cli.b('option b description')
cli.c(args: '*', 'option c description')
cli.usage()
----
Gives this output:
image::assets/img/usageMessageSpec.png[]
*New property: parser*
The `parser` property gives access to the picocli `ParserSpec` object that can be used to customize the parser behavior.
See the http://picocli.info/apidocs/picocli/CommandLine.Model.ParserSpec.html[documentation] for details.
*Map options*
Finally, if your application has options that are key-value pairs, you may be interested in picocli's support for maps. For example:
[source,groovy]
----
import java.util.concurrent.TimeUnit
import static java.util.concurrent.TimeUnit.DAYS
import static java.util.concurrent.TimeUnit.HOURS
def cli = new CliBuilder()
cli.D(args: 2, valueSeparator: '=', 'the old way') // <1>
cli.X(type: Map, 'the new way') // <2>
cli.Z(type: Map, auxiliaryTypes: [TimeUnit, Integer].toArray(), 'typed map') // <3>
def options = cli.parse('-Da=b -Dc=d -Xx=y -Xi=j -ZDAYS=2 -ZHOURS=23'.split())// <4>
assert options.Ds == ['a', 'b', 'c', 'd'] // <5>
assert options.Xs == [ 'x':'y', 'i':'j' ] // <6>
assert options.Zs == [ (DAYS as TimeUnit):2, (HOURS as TimeUnit):23 ] // <7>
----
<1> Previously, `key=value` pairs were split up into parts and added to a list
<2> Picocli map support: simply specify `Map` as the type of the option
<3> You can even specify the type of the map elements
<4> To compare, let's specify two key-value pairs for each option
<5> Previously, all key-value pairs end up in a list and it is up to the application to work with this list
<6> Picocli returns the key-value pairs as a `Map`
<7> Both keys and values of the map can be strongly typed
==== ObjectGraphBuilder
`ObjectGraphBuilder` is a builder for an arbitrary graph of beans that
Expand Down
25 changes: 25 additions & 0 deletions subprojects/groovy-cli-picocli/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 "info.picocli:picocli:$picocliVersion"
// compile files ('C:/Users/remko/IdeaProjects/picocli/build/libs/picocli-3.0.0-beta-3-SNAPSHOT.jar')
testCompile project(':groovy-test')
testCompile project(':groovy-dateutil')
}
Loading

0 comments on commit 237f8cc

Please sign in to comment.