Clojure Cookbook: Parsing Command-Line Arguments
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:defaultisnil.:flag: If truthy (notfalseornil), 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
- Running Programs from the Command Line, to learn more about invoking applications from the command line
- Writing to STDOUT and STDERR, to learn about input and output streams
- Packaging a Project into a JAR File, to learn how to package an application as an executable JAR file
- For building ncurses-style applications, see clojure-lanterna, a wrapper around the Lanterna terminal output library
Like this post? Subscribe to my newsletter.
Get fresh content on Clojure, Architecture and Software Development, each and every week.