Building CLIs with Groovy's CLI Builder
Howdy, friends! You know what’s better than heavy lifting, great performing (forgetting about startup expense), scripts and command line tools on the JVM? You guessed it – ones that provide well documented and user-friendly parameters and interfaces for knob spinning and tweaking.
So, then, you’ve probably guessed this post is about Groovy’s CliBuilder
. You. are. CORRECT!
The referenced Groovy/Java doc is actually pretty good, but I wanted to share some examples and pro tips… Let me introduce you to my littleFriend
:
./littleFriend -h
usage: littleFriend
Little Friend CLI
-dt,--doubleThing <arg> Some double. [defaults to '1.09']
-h,--help Usage Information
-nt,--numberThing <arg> Some number. [defaults to '6200']
-st,--stringThing <arg> Some string. [defaults to 'How now little friend?']
littleFriend
is a thing of wonder – helping you and I understand CliBuilder
. Here we have a little utility that takes 3 optional parameters/arguments:
stringThing
(also-st
)doubleThing
(also-dt
)numberThing
(also-nt
)
… and has another flag -h
or --help
which is parameter-less that renders the prior help menu. The code for this straight forward:
#!/usr/bin/env groovy
def
def defaultStringThing = 'How now little friend?'
def defaultDoubleThing = '1.09'
def defaultNumberThing = "${1550 * 4}"
def cli = new CliBuilder(header: 'Little Friend CLI', usage:'littleFriend', width: -1)
cli.st(longOpt: 'stringThing', "Some string. [defaults to '${defaultStringThing}']", args: 1, defaultValue: defaultStringThing)
cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, defaultValue: defaultDoubleThing)
cli.nt(longOpt: 'numberThing', "Some number. [defaults to '${defaultNumberThing}']", args: 1, defaultValue: defaultNumberThing)
cli.h(longOpt: 'help', 'Usage Information')
def cliOptions = cli.parse(args)
if (!cliOptions) {
cli.usage()
System.exit(-1)
}
if (cliOptions.help) {
cli.usage()
System.exit(0)
}
There are a few things to point out:
- CLI options always have a short param ex (
cli.st
) and optionally have a long optioncli.st(longOpt: 'stringThing' ...
- Default values for parameterized options must always be strings, ex:
def defaultNumberThing = "${1550 * 4}"
- You must cast each value pulled from CliBuilder after parsing unless you’ve specified a type or converter
- If you use a convert you cannot specify type
- Validation, even printing the usage in a user-friendly format must be done manually – that is the reason for the conditionals checking for parsing and the help menu. (It is also worth nothing the help CLI option could just as easily be any boolean flag for any other purpose.)
The following examples renders the values from the parsed options and their types…
#!/usr/bin/env groovy
def defaultStringThing = 'How now little friend?'
def defaultDoubleThing = '1.09'
def defaultNumberThing = "${1550 * 4}"
def cli = new CliBuilder(header: 'Little Friend CLI', usage:'littleFriend', width: -1)
cli.st(longOpt: 'stringThing', "Some string. [defaults to '${defaultStringThing}']", args: 1, defaultValue: defaultStringThing)
cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, defaultValue: defaultDoubleThing)
cli.nt(longOpt: 'numberThing', "Some number. [defaults to '${defaultNumberThing}']", args: 1, defaultValue: defaultNumberThing)
cli.h(longOpt: 'help', 'Usage Information')
def cliOptions = cli.parse(args)
if (!cliOptions) {
cli.usage()
System.exit(-1)
}
if (cliOptions.help) {
cli.usage()
System.exit(0)
}
println "stringThing is '${cliOptions.stringThing}' of type ${cliOptions.stringThing.getClass()}"
println "doubleThing is '${cliOptions.doubleThing}' of type ${cliOptions.doubleThing.getClass()}"
println "numberThing is '${cliOptions.numberThing}' of type ${cliOptions.numberThing.getClass()}"
…yields the following output:
./littleFriend
stringThing is 'How now little friend?' of type class java.lang.String
doubleThing is '1.09' of type class java.lang.String
numberThing is '6200' of type class java.lang.String
Notice they are all of type String.
Making littleFriend
Friendly
Types are important and you can just as easily check and cast things in your code after processing…
#!/usr/bin/env groovy
def defaultStringThing = 'How now little friend?'
def defaultDoubleThing = '1.09'
def defaultNumberThing = "${1550 * 4}"
def cli = new CliBuilder(header: 'Little Friend CLI', usage:'littleFriend', width: -1)
cli.st(longOpt: 'stringThing', "Some string. [defaults to '${defaultStringThing}']", args: 1, defaultValue: defaultStringThing)
cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, defaultValue: defaultDoubleThing)
cli.nt(longOpt: 'numberThing', "Some number. [defaults to '${defaultNumberThing}']", args: 1, defaultValue: defaultNumberThing)
cli.h(longOpt: 'help', 'Usage Information')
def cliOptions = cli.parse(args)
if (!cliOptions) {
cli.usage()
System.exit(-1)
}
if (cliOptions.help) {
cli.usage()
System.exit(0)
}
println "stringThing is '${cliOptions.stringThing}' of type ${cliOptions.stringThing.getClass()}"
println "doubleThing is '${cliOptions.doubleThing}' of type ${cliOptions.doubleThing.getClass()}"
println "numberThing is '${cliOptions.numberThing}' of type ${cliOptions.numberThing.getClass()}"
def doubleThing = cliOptions.doubleThing as Double
println "actual doubleThing is '${doubleThing}' of type ${doubleThing.getClass()}"
…yields the following output:
./littleFriend
stringThing is 'How now little friend?' of type class java.lang.String
doubleThing is '1.09' of type class java.lang.String
numberThing is '6200' of type class java.lang.String
actual doubleThing is '1.09' of type class java.lang.Double
Notice the second to final non-empty line takes the String doubleThing
and converts it to a double using Groovy’s as
keyword.
Making littleFriend
Friendlier
A better way, though, which is less code and allows the parser to enforce correctness, etc… is to set the type on the option declaration itself.
#!/usr/bin/env groovy
def defaultStringThing = 'How now little friend?'
def defaultDoubleThing = '1.09'
def defaultNumberThing = "${1550 * 4}"
def cli = new CliBuilder(header: 'Little Friend CLI', usage:'littleFriend', width: -1)
cli.st(longOpt: 'stringThing', "Some string. [defaults to '${defaultStringThing}']", args: 1, defaultValue: defaultStringThing)
cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, type: Double, defaultValue: defaultDoubleThing)
cli.nt(longOpt: 'numberThing', "Some number. [defaults to '${defaultNumberThing}']", args: 1, defaultValue: defaultNumberThing)
cli.h(longOpt: 'help', 'Usage Information')
def cliOptions = cli.parse(args)
if (!cliOptions) {
cli.usage()
System.exit(-1)
}
if (cliOptions.help) {
cli.usage()
System.exit(0)
}
println "stringThing is '${cliOptions.stringThing}' of type ${cliOptions.stringThing.getClass()}"
println "doubleThing is '${cliOptions.doubleThing}' of type ${cliOptions.doubleThing.getClass()}"
println "numberThing is '${cliOptions.numberThing}' of type ${cliOptions.numberThing.getClass()}"
…yields the following output:
./littleFriend
stringThing is 'How now little friend?' of type class java.lang.String
doubleThing is '1.09' of type class java.lang.Double
numberThing is '6200' of type class java.lang.String
Noting the line cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, defaultValue: defaultDoubleThing)
has been modified to cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, type: Double, defaultValue: defaultDoubleThing)
, where the type
is declared.
Personalized Friendliness
When supplying converters, types cannot be used and casting isn’t required. Here we have a class Person
and a converted, as a closure, personConverter
, that takes an input as input
and returns a newly constructed Person
.
#!/usr/bin/env groovy
import groovy.transform.Canonical
def defaultStringThing = 'How now little friend?'
def defaultDoubleThing = '1.09'
def defaultNumberThing = "${1550 * 4}"
@Canonical class Person {
def name
}
def personConverter = { input ->
new Person(input)
}
def cli = new CliBuilder(header: 'Little Friend CLI', usage:'littleFriend', width: -1)
cli.st(longOpt: 'stringThing', "Some string. [defaults to '${defaultStringThing}']", args: 1, defaultValue: defaultStringThing)
cli.dt(longOpt: 'doubleThing', "Some double. [defaults to '${defaultDoubleThing}']", args: 1, type: Double, defaultValue: defaultDoubleThing)
cli.nt(longOpt: 'numberThing', "Some number. [defaults to '${defaultNumberThing}']", args: 1, defaultValue: defaultNumberThing)
cli.pt(longOpt: 'personThing', "Some person.", args: 1, convert: personConverter)
cli.h(longOpt: 'help', 'Usage Information')
def cliOptions = cli.parse(args)
if (!cliOptions) {
cli.usage()
System.exit(-1)
}
if (cliOptions.help) {
cli.usage()
System.exit(0)
}
println "stringThing is '${cliOptions.stringThing}' of type ${cliOptions.stringThing.getClass()}"
println "doubleThing is '${cliOptions.doubleThing}' of type ${cliOptions.doubleThing.getClass()}"
println "numberThing is '${cliOptions.numberThing}' of type ${cliOptions.numberThing.getClass()}"
if (cliOptions.personThing) {
println "personThing is '${cliOptions.personThing}' of type ${cliOptions.personThing.getClass()}"
}
…yields the following output:
./littleFriend -pt hi
stringThing is 'How now little friend?' of type class java.lang.String
doubleThing is '1.09' of type class java.lang.Double
numberThing is '6200' of type class java.lang.String
personThing is 'Person(hi)' of type class Person
Cheers!