Skip to content

Instantly share code, notes, and snippets.

@hanslovsky
Last active December 24, 2019 21:14
Show Gist options
  • Save hanslovsky/8276da86c53bc6d95bf01447cd5cb2b7 to your computer and use it in GitHub Desktop.
Save hanslovsky/8276da86c53bc6d95bf01447cd5cb2b7 to your computer and use it in GitHub Desktop.
Add (multiple) structured objects with picocli (https://picocli.info) on the command line. Requires kscript: https://github.com/holgerbrandl/kscript

Structured Objects

In some use cases it can be useful to add or configure structured objects from the command line. For example, to add a dataset to a visualization tool at start-up, multiple parameters need to be specified: the location of the dataset, a contrast range, what kind of data it is, etc. The amazing Java CLI parser picocli added the ArgGroup annotation in version 4.0.0 that we can use to configure/add structured objects from the command line.

In the following, I will use the Kotlin programming language to demonstrate how to parse structured objects with picocli.

First, we define a structured object:

data class StructuredObject(val text: String, val floatingPoint: Double?, val integer: Int?)

Next, we create a class with annotated options for each of the members of StructuredObject. In this case, we require the --text option:

class StructuredObjectOptions {
    @CommandLine.Option(names = ["--text"], required = true)
    lateinit var text: String
        private set

    @CommandLine.Option(names = ["--float"])
    var floatingPoint: Double? = null
        private set

    @CommandLine.Option(names = ["--int"])
    var integer: Int? = null
        private set
}

The StructuredObjectOptions are then grouped with a dummy option to "trigger" the configuration of a StructuredObject in the class StructuredObjectGroup:

class StructuredObjectGroup {
    @CommandLine.Option(names = ["--structured-object", "-s"], required = true)
    private var dummy: Boolean = false

    @CommandLine.ArgGroup(multiplicity = "1", exclusive = false)
    private lateinit var options: StructuredObjectOptions

    val structuredObject: StructuredObject
        get() = StructuredObject(options.text, options.floatingPoint, options.integer)
}

The CommandLine.ArgGroup annotation with multiplicity = "1" ensures that the StructuredObject is configured properly for each --structured-object, -s that is passed as argument. If StructuredObjectOptions does not have any mandatory parameters (required = false), it is probably best to set multiplicity = "0..1", here. The exclusive = false option is necessary to configure multiple options at the same time.

Finally, the CommandLineArgs class

@CommandLine.Command(name = "picocli-structured-objects")
class CommandLineArgs : Callable<Unit> {
    @CommandLine.ArgGroup(multiplicity = "0..*", exclusive = false)
    private lateinit var _options: Array<StructuredObjectGroup>

    @CommandLine.Option(names = ["--help", "-h"], usageHelp = true)
    var helpRequested: Boolean = false
        private set

    val options: Array<StructuredObjectGroup>
        get() = if(this::_options.isInitialized) _options else arrayOf()

    override fun call() = options.forEach { println(it.structuredObject) }
}

adds the StructuredObjectGroup as an ArgGroup with multiplicity = "0..*" to allow for an arbitrary number of structured objects to be be added/configured through the CLI, for example:

CommandLine(CommandLineArgs()).execute("-s", "--text", "abc", "-s", "--text", "def", "--float=1.3", "-s", "--text", "xyz", "--int=-1", "-s", "--text", "kotlin", "--float=3.14159265359", "--int=7")
StructuredObject(text=abc, floatingPoint=null, integer=null)
StructuredObject(text=def, floatingPoint=1.3, integer=null)
StructuredObject(text=xyz, floatingPoint=null, integer=-1)
StructuredObject(text=kotlin, floatingPoint=3.14159265359, integer=7)

I added a fully-functional kscript example and example invocations with the expected outputs. For further reading, please see the picocli docs.

Caveats

As far as I know, there are two major caveats with this approach that uses ArgGroup annotations in a somewhat hacky way to allow for configuration of (multiple) structured objects through the command line:

  1. picocli does not enforce any order in which the options are passed, in particular passing --text def --float=1.3 -s and -s --text def --float=1.3 are the same. This is counter-intuitive for this use case, where the -s option "triggers" configuration of a structured object but is probably only a minor issue because users will usually start with the -s option.
  2. Multiple use of the same option flag is not permitted. For example, adding a "--text" option to the CommandLineArgs class results in this runtime error:
Option name '--text' is used by both field String Picocli_structured_objects$CommandLineArgs.text and field String Picocli_structured_objects$StructuredObjectOptions.text
$ picocli-structured-objects.kts --help
Usage: picocli-structured-objects [-s (--text=<text> [--float=<floatingPoint>]
[--int=<integer>])]... [-h]
--float=<floatingPoint>
-h, --help
--int=<integer>
-s, --structured-object
--text=<text>
$ picocli-structured-objects.kts
$ picocli-structured-objects.kts -s --text abc -s --text def --float=1.3 -s --text xyz --int=-1 -s --text kotlin --float=3.14159265359 --int=7
StructuredObject(text=abc, floatingPoint=null, integer=null)
StructuredObject(text=def, floatingPoint=1.3, integer=null)
StructuredObject(text=xyz, floatingPoint=null, integer=-1)
StructuredObject(text=kotlin, floatingPoint=3.14159265359, integer=7)
$ picocli-structured-objects.kts -s --text abc -s
Error: Missing required argument(s): (--text=<text> [--float=<floatingPoint>] [--int=<integer>])
Usage: picocli-structured-objects [-s (--text=<text> [--float=<floatingPoint>]
[--int=<integer>])]... [-h]
--float=<floatingPoint>
-h, --help
--int=<integer>
-s, --structured-object
--text=<text>
# caveat 1: option order not enforced
$ picocli-structured-objects.kts --text def -s --float=1.3
StructuredObject(text=def, floatingPoint=1.3, integer=null)
$ picocli-structured-objects.kts -s --text def --float=1.3
StructuredObject(text=def, floatingPoint=1.3, integer=null)
$ picocli-structured-objects.kts --text def --float=1.3 -s
StructuredObject(text=def, floatingPoint=1.3, integer=null)
$ picocli-structured-objects.kts --text def -s --float=1.3 --int=1 -s --text ukulele
StructuredObject(text=def, floatingPoint=1.3, integer=1)
StructuredObject(text=ukulele, floatingPoint=null, integer=null)
#!/usr/bin/env kscript
@file:DependsOn("info.picocli:picocli:4.0.3")
import java.util.concurrent.Callable
import picocli.CommandLine
data class StructuredObject(val text: String, val floatingPoint: Double?, val integer: Int?)
class StructuredObjectOptions {
@CommandLine.Option(names = ["--text"], required = true)
lateinit var text: String
private set
@CommandLine.Option(names = ["--float"])
var floatingPoint: Double? = null
private set
@CommandLine.Option(names = ["--int"])
var integer: Int? = null
private set
}
class StructuredObjectGroup {
@CommandLine.Option(names = ["--structured-object", "-s"], required = true)
private var dummy: Boolean = false
@CommandLine.ArgGroup(multiplicity = "1", exclusive = false)
private lateinit var options: StructuredObjectOptions
val structuredObject: StructuredObject
get() = StructuredObject(options.text, options.floatingPoint, options.integer)
}
@CommandLine.Command(name = "picocli-structured-objects")
class CommandLineArgs : Callable<Unit> {
@CommandLine.ArgGroup(multiplicity = "0..*", exclusive = false)
private lateinit var _options: Array<StructuredObjectGroup>
@CommandLine.Option(names = ["--help", "-h"], usageHelp = true)
var helpRequested: Boolean = false
private set
val options: Array<StructuredObjectGroup>
get() = if(this::_options.isInitialized) _options else arrayOf()
override fun call() = options.forEach { println(it.structuredObject) }
}
CommandLine(CommandLineArgs()).execute(*args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment