Monday, September 21, 2009

Groovy Goodness: Parsing Commandline Arguments with CliBuilder

Groovy is a great language to create scripts. Most of the time if we invoke the scripts we pass arguments to the script. These arguments are available as a String[] array in our script. For example to get the first argument we can use the following code s = args[0]. To have real argument handling in our script we use Groovy's CliBuilder class. This class uses Jakarta Commons CLI under the hood. With CliBuilder we can define the argument options and parse the arguments. CliBuilder adds Groovyness by allowing us to invoke non-existing one-letter methods, which are turned into argument options with the one-letter shortcut. Furthermore we can use named parameters to define usage text, parser implementation, formatter implementation and option properties.

Let's see the CliBuilder in action. We create a script to show a date formatted according to defined arguments. If we don't define a date the current date is assumed. And furthermore we want to be able to define a prefix text, which is added before the formatted date (is empty by default).

import java.text.*

def showdate(args) {
    def cli = new CliBuilder(usage: 'showdate.groovy -[chflms] [date] [prefix]')
    // Create the list of options.
    cli.with {
        h longOpt: 'help', 'Show usage information'
        c longOpt: 'format-custom', args: 1, argName: 'format', 'Format date with custom format defined by "format"'
        f longOpt: 'format-full',   'Use DateFormat#FULL format'
        l longOpt: 'format-long',   'Use DateFormat#LONG format'
        m longOpt: 'format-medium', 'Use DateFormat#MEDIUM format (default)'
        s longOpt: 'format-short',  'Use DateFormat#SHORT format'
    def options = cli.parse(args)
    if (!options) {
    // Show usage text when -h or --help option is used.
    if (options.h) {
        // Will output:
        // usage: showdate.groovy -[chflms] [date] [prefix]
        //  -c,--format-custom <format>   Format date with custom format defined by "format"
        //  -f,--format-full              Use DateFormat#FULL format   
        //  -h,--help                     Show usage information   
        //  -l,--format-long              Use DateFormat#LONG format   
        //  -m,--format-medium            Use DateFormat#MEDIUM format   
        //  -s,--format-short             Use DateFormat#SHORT format   
    // Determine formatter.
    def df = DateFormat.getDateInstance(DateFormat.MEDIUM)  // Defeault.
    if (options.f) {  // Using short option.
        df = DateFormat.getDateInstance(DateFormat.FULL) 
    } else if (options.'format-long') {  // Using long option.
        df = DateFormat.getDateInstance(DateFormat.LONG) 
    } else if (options.'format-medium') {
        df = DateFormat.getDateInstance(DateFormat.MEDIUM) 
    } else if (options.s) {
        df = DateFormat.getDateInstance(DateFormat.SHORT) 
    } else if (options.'format-custom') {
        df = new SimpleDateFormat(options.c)

    // Handle all non-option arguments.
    def prefix = ''  // Default is empty prefix.
    def date = new Date()  // Default is current date.
    def extraArguments = options.arguments()
    if (extraArguments) {
        date = new Date().parse(extraArguments[0])
        // The rest of the arguments belong to the prefix.
        if (extraArguments.size() > 1) {
            prefix = extraArguments[1..-1].join(' ')

// Set locale for assertions.
assert '12/1/09' == showdate(['--format-short', '2009/12/1'])
assert '12/1/09' == showdate(['-s', '2009/12/1'])
assert 'Dec 1, 2009' == showdate(['2009/12/1'])
assert 'Dec 1, 2009' == showdate(['--format-medium', '2009/12/1'])
assert 'Dec 1, 2009' == showdate(['-m', '2009/12/1'])
assert 'December 1, 2009' == showdate(['--format-long', '2009/12/1'])
assert 'December 1, 2009' == showdate(['-l', '2009/12/1'])
assert 'Tuesday, December 1, 2009' == showdate(['--format-full', '2009/12/1'])
assert 'Tuesday, December 1, 2009' == showdate(['-f', '2009/12/1'])
assert 'Default date format: Dec 1, 2009' == showdate(['2009/12/1', 'Default', 'date', 'format: '])
assert 'Important date: Dec 1, 2009' == showdate(['-m', '2009/12/1', 'Important date: '])
assert 'week 49 of the year 2009 AD' == showdate(['-c', "'week' w 'of the year' yyyy G", '2009/12/1'])
assert '2009/12/01' == showdate(['--format-custom', 'yyyy/MM/dd', '2009/12/01'])
assert '2009' == showdate(['-cyyyy', '2009/12/1'])
assert new Date().format('yyyy/MM/dd') == showdate(['--format-custom', 'yyyy/MM/dd'])

println showdate(args) 


Christophe said...

if you'd like to reproduce the shell $0 var in groovy, you can access the script name using this.class.name. Just add .groovy and here you are.

new CliBuilder(usage: "${this.class.name}.groovy -[chflms] [date] [prefix]")

You can now change you script file name without having to change it inside your CliBuilder constructor.

mrhaki said...

@Christophe: thanks, that is Groovy!

Jarrod said...

There is a much better way to parse command line arguments in Groovy via the Java Simple Argument Parser library (JSAP).


mrhaki said...

@Jarrod: thanks, I didn't know about the JSAP library.

Tomek said...

Thanks, this post helped me to write my first command line parsing snippet in an instant!

Marty N. said...

The asserts on lines 72 and 73 fail, as there is no space after the colon for the rhs.

Hubert Klein Ikkink said...

@Marty N.: Thank you spotting this. I've changed the code and added a space ofter the colon.

Post a Comment