Clojure Cookbook: Parsing Command-Line Arguments

  • clojure
  • cookbook

In this week’s Clojure Cookbook preview recipe, we’ll be taking a look at tools.cli, a nifty core library for parsing command-line arguments.

This recipe was really fun to write because of how simple tools.cli is. Since parsing string arguments is decomplected from the actual act of running a program at the command-line, it is completely possible to build and test a tools.cli parser at the REPL.

I highly encourage you to follow along with this one.

Parsing Command-Line Arguments

by Ryan Neufeld; originally submitted by Nicolas Bessi

Problem

You want to write command-line tools in Clojure that can parse input arguments.

Solution

Use the tools.cli library.

Before starting, add [org.clojure/tools.cli "0.2.4"] to your project’s dependencies, or start a REPL using lein-try:

$ lein try org.clojure/tools.cli

Use the clojure.tools.cli/cli function in your project’s -main function entry point to parse command-line arguments. Since tools.cli is so cool, this example can run entirely at the REPL.

(require '[clojure.tools.cli :refer [cli]])

(defn -main [& args]
  (let [[opts args banner] (cli args
                                ["-h" "--help" "Print this help"
                                 :default false :flag true])]
    (when (:help opts)
      (println banner))))

;; Simulate entry into -main at the command line
(-main "-h")
;; *out*
;; Usage:
;;
;;  Switches                 Default  Desc
;;  ``````                 ```---  ```
;;  -h, --no-help, --help    false    Print this help

Discussion

Clojure’s tools.cli is a simple library, with only one function, cli, and a slim data-oriented API for specifying how arguments should be parsed. Handily enough, there isn’t much special about this function: an arguments vector and specifications go in, and a map of parsed options, variadic arguments, and a help banner come out. It’s really the epitome of good, composable functional programming.

To configure how options are parsed, pass any number of spec vectors after the args list. To specify a :port parameter, for example, you would provide the spec ["-p" "--port"]. The "-p" isn’t strictly necessary, but it is customary to provide a single-letter shortcut for command-line options (especially long ones). In the returned opts map, the text of the last option name will be interned to a keyword (less the --). For example, "--port" would become :port, and "--super-long-option" would become :super-long-option.

If you’re a polite command-line application developer, you’ll also include a description for each of your options. Specify this as an optional string following the final argument name:

["-p" "--port" "The incoming port the application will listen on."]

Everything after the argument name and description will be interpreted as options in key/value pairs. tools.cli provides the following options:

  • :default: The default value returned in the absence of user input. Without specifying, the default of :default is nil.

  • :flag: If truthy (not false or nil), indicates an argument behaves like a flag or switch. This argument will not take any value as its input.

  • :parse-fn: The function used to parse an argument’s value. This can be used to turn string values into integers, floats, or other data types.

  • :assoc-fn: The function used to combine multiple values for a single argument.

Here’s a complete example:

(def app-specs [["-n" "--count" :default 5
                                :parse-fn #(Integer. %)
                                :assoc-fn max]
                ["-v" "--verbose" :flag true
                                  :default true]])

(first (apply cli ["-n" "2" "-n" "50"] app-specs))
;; -> {:count 50, :verbose true}

(first (apply cli ["--no-verbose"] app-specs))
;; -> {:count 5, :verbose false}

When writing flag options, a useful shortcut is to omit the :flag option and add a “[no-]” prefix to the argument’s name. cli will interpret this argument spec as including :flag true without you having to specify it as such:

["-v" "--[no-]verbose" :default true]

One thing the tools.cli library doesn’t provide is a hook into the application container’s launch life cycle. It is your responsibility to add a cli call to your -main function and know when to print the help banner. A general pattern for use is to capture the results of cli in a let block and determine if help needs to be printed. This is also useful for ensuring the validity of arguments (especially since there is no :required option):

(def required-opts #{:port})

(defn missing-required?
  "Returns true if opts is missing any of the required-opts"
  [opts]
  (not-every? opts required-opts))

(defn -main [& args]
  (let [[opts args banner] (cli args
                                ["-h" "--help" "Print this help"
                                 :default false :flag true]
                                ["-p" "--port" :parse-fn #(Integer. %)])]
    (when (or (:help opts)
              (missing-required? opts))
        (println banner))))

As with many applications, you may want to accept a variable number of arguments; for example, a list of filenames. In most cases, you don’t need to do anything special to capture these arguments–just supply them after any other options. These variadic arguments will be returned as the second item in cli’s returned vector:

(second (apply cli ["-n" "5" "foo.txt" "bar.txt"] app-specs))
;; -> ["foo.txt" "bar.txt"]

If your variadic arguments look like flags, however, you’ll need another trick. Use -- as an argument to indicate to cli that everything that follows is a variadic argument. This is useful if you’re invoking another program with the options originally passed to your program:

(second (apply cli ["-n" "5" "--port" "80"] app-specs))
;; -> Exception '--port' is not a valid argument ...

(second (apply cli ["-n" "5" "--" "--port" "80"] app-specs))
;; -> ["--port" "80"]

Once you’ve finished toying with your application’s option parsing at the REPL, you’ll probably want to try invoking options via lein run. Just like your application needs to use -- to indicate arguments to pass on to subsequent programs, so too must you use -- to indicate to lein run which arguments are for your program and which are for it:

# If app-specs were rigged up to a project...
$ lein run -- -n 5 --no-verbose

See Also

Like this post? Subscribe to my newsletter.

Get fresh content on Clojure, Architecture and Software Development, each and every week.

comments powered by Disqus