Task Options DSL

Andrea Richiardi edited this page Jan 12, 2018 · 131 revisions
Clone this wiki locally

Boot tasks are intended to be used from the command line, from a REPL, in the project's build.boot file, or from regular Clojure namespaces. This requires support for two conceptually different calling conventions: with command line options and optargs, and as s-expressions in Clojure with argument values.

Furthermore, since tasks are boot's user interface it's important that they provide good usage and help documentation in both environments.

Example

To get an idea of how this correspondence works consider the following simple example task (you can start a boot REPL and type it in):

(deftask options
  "Demonstrate the task options DSL."
  [a a-option VAL  kw    "The option."
   c counter       int   "The counter."
   e entry    VAL  sym   "An entrypoint symbol."
   f flag          bool  "Enable flag."
   o o-option VAL  str   "The other option."]
  *opts*)
boot.user=> (options "-h")
Demonstrate the task options DSL.

Options:
  -h, --help          Print this help info.
  -a, --a-option VAL  Set the option to VAL.
  -c, --counter       Increase the counter.
  -e, --entry VAL     Set an entrypoint symbol to VAL.
  -f, --flag          Enable flag.
  -o, --o-option VAL  Set the other option to VAL.
nil
boot.user=> (doc options)
-------------------------
boot.user/options
([& {:keys [help a-option counter flag o-option], :as *opts*}])
  Demonstrate the task options DSL.

  Keyword Args:
    :help      bool  Print this help info.
    :a-option  kw    The option.
    :counter   int   The counter.
    :entry     sym   An entrypoint symbol.
    :flag      bool  Enable flag.
    :o-option  str   The other option.
nil
boot.user=> (options "-a" "foo")
{:a-option :foo}
boot.user=> (options "--a-option" "foo")
{:a-option :foo}
boot.user=> (options :a-option :foo)
{:a-option :foo}
boot.user=> (options "-ccfa" "foo" "-o" "bar")
{:counter 2, :flag true, :a-option :foo, :o-option "bar"}
boot.user=> (options :counter 2 :flag true :a-option :foo :o-option "bar")
{:counter 2, :flag true, :a-option :foo, :o-option "bar"}

Anatomy

Boot tasks are defined with the deftask macro. This macro defines functions that can be called with either keyword arguments or command-line arguments as strings. Boot exploits this property of tasks when pipelines are created on the command line.

Note: Prior to version 2.7.0 tasks only took keyword arguments / command line options. The support for positional parameters is rudimentary and has a few gotchas (see below). In general, prefer using keyword arguments.

As with defn, arguments in deftask are declared in an arglist vector. The arglist for deftask is not, however, a simple vector of binding forms; it is more similar to the specification passed to tools.cli. Each argument is represented by a number of symbols and a string:

(deftask foo
  "Does foo."
  [f foo FOO str "The foo option."
   ↑  ↑   ↑   ↑          ↑
   1  2   3   4          5
   ...
  1. Short name – one-character, must be unique (h is reserved by boot)
  • Use _ for options with no short name
  1. Long name – multi-character, must be unique (help is reserved by boot)
  2. Optarg – if provided, indicates that option expects argument (vs. flag)
  3. Type – the Clojure data type hint for the option value (see below)
  4. Description – incorporated into command line help output and docstring

Options values are bound in the deftask body to:

  • The long option name – (eg. foo in the example above)
  • The *opts* catchall – a map of options to values.

Additionally, a function that will print the task usage help info is bound to *usage* in the task body. This can be used by the task like this:

(deftask foo
  [b bar VALUE int "The bar option."]
  (if-not bar
    (do (boot.util/fail "The -b/--bar option is required!") (*usage*))
    ...

Positional Parameters

Tasks can also take positional parameters. These cannot be declared in the argument vector and they will all be collected into a sequence bound to *args*.

(deftask print-args []
  (prn *args*)
  identity)

(boot (print-args "1" "2" "3"))
;; ("1" "2" "3")

Passing non-string parameters is possible, but currently (2017-02-08) requires a bit of a hack. The string -- is used to signal the end of the keyword arguments to the parser.

(boot (print-args "--" 1 2 3))
;; (1 2 3)

You can also pass positional arguments from the command line. Note that all values will be strings and there's some extra syntax needed. You must wrap the entire task in [] so boot doesn't interpret the positional args as a list of task names.

$ boot [ print-args 1 2 3 ]
;; ("1" "2" "3")

Passing flag-like positional arguments

One common usage of positional arguments is passing arguments through to something else, like the -main function of a namespace. If these arguments contain things that look like boot flags (i.e. they start with a -), you need to use the -- separator so boot doesn't throw an error when it encounters an unrecognized flag.

$ boot [ print-args -- --pass-through=args 2 3 -f ]
;; ("--pass-through=args" "2" "3" "-f")

Optargs

The optarg is an optional placeholder that indicates that the associated option takes a required argument. This is in contrast with flag or counter type options that take no arguments.

The foo above is a simple example of an option with a required argument. For more complex applications the DSL provides a mechanism by which additional structure can be encoded in the optarg.

Note that the optarg is a pattern only. The value of an option, as mentioned in the preceding section, is bound to the long option name. The more elaborate forms of the pattern are discussed in the section on Complex Options.

Types

Option type declarations provide a concise language specifying how command line arguments are parsed. They also serve as templates for automatic documentation and validation of task options.

type hint parse fn validate fn
bool identity #{true false}
char first char?
code (comp eval read-string) (constantly true)
edn read-string (constantly true)
file clojure.java.io/file #(instance? File %)
float read-string float?
int read-string integer?
kw keyword keyword?
regex re-pattern #(instance? Pattern %)
str identity string?
sym symbol symbol?

Options can also be declared as collections of these primitive types. Sets, maps, and vectors are supported. There is an obvious limit to the complexity of option types on the command line; only the most useful and straightforward patterns are supported.

In some cases you may want apply validation and parsing only from command line. To achieve this simply place ^:! as metadata right before the type expression. This is similar in power to using edn or code, but can give a better CLI experience.

Examples

Rather than attempt a rigorous specification of the DSL we provide a number of examples to illustrate intuitively how it is used. The example deftasks should be understood to return *opts*.

Flags

Flags are boolean options. They take no arguments–their presence or absence is enough to determine their value.

Note: no optarg is specified because this option takes no argument.

(deftask foo
  "Does foo."
  [f foo bool "Enable foo behavior."
   ...
boot.user=> (foo "-f")
{:foo true}

Counters

Counters are options which, like flags, take no arguments. Their value is the number of times the option was given on the command line.

Note: no optarg is specified because this option takes no argument.

(deftask foo
  "Does foo."
  [f foo int "The number of foos."
   ...
boot.user=> (foo "-fff")
{:foo 3}

Simple Options

Simple options have a single, required argument.

(deftask foo
  "Does foo."
  [f foo LEVEL int "The initial foo level."
   ...
boot.user=> (foo "-f" "100")
{:foo 100}

Multi Options

Options can also be sets or vectors. Multiple use of the option on the command line conjes items onto the set or vector.

(deftask foo
  "Does foo."
  [f foo LEVELS #{int} "The set of initial foo levels."
   ...
boot.user=> (foo "-f" "100" "-f" "200" "-f" "300")
{:foo #{100, 200, 300}}

Complex Options

Option values can also be collections of collections. They can be maps, sets of vectors, or vectors of vectors.

(deftask foo
  "Does foo."
  [f foo FOO=BAR {kw sym} "The foo option."
            ↑       ↑
            1       2
   ...
boot.user=> (foo "-f" "bar=baz")
{:foo {:bar baz}}
boot.user=> (foo "-f" "bar=baz" "-f" "baf=quux")
{:foo {:bar baz :baf quux}}
  1. Splitting – This shows "splitting" of the optarg on the = character. In fact, any non alpha-numeric character in the optarg placeholder is interpreted as a character to split on. The splitting produces a vector.

  2. Conjing – Each use of the option on the command line conjes the split value vector onto the collection map.

A set of vector triples:

(deftask foo
  "Does foo."
  [f foo FOO=BAR:BAZ #{[kw sym str]} "The foo option."
   ...
boot.user=> (foo "-f" "bar=baz:baf")
{:foo #{[:bar baz "baf"]}}
boot.user=> (foo "-f" "bar=baz:baf" "-f" "baf=quux:xyzzy")
{:foo #{[:bar baz "baf"] [:baf quux "xyzzy"]}}

Sequential Optargs

Sequential, not nested optarg types are supported as well.

(deftask foo
  "Does foo."
  [f foo FOO=BAR:BAZ [kw sym str] "The foo option." ;; not nested in a map anymore
   ...
boot.user=> (foo "-f" "bar=baz:baf")
{:foo [:bar baz "baf"]}

Note that in boot < 2.8.0 sequential types fail with:

java.lang.ClassCastException: clojure.lang.PersistentVector cannot be cast to java.lang.String

If multiple arguments are supplied only there last is taken and no conjoining happens:

boot.user=> (foo "-f" "overridden:X.X=uninteresting" "-f" "bar=baz:baf")
{:foo [:bar baz "baf"]}