Building Command Applications with Clojure
To be able to package your Clojure application as a single jar and to run it from a script or the command line is a very handy thing. At first it might not be all that apparent how to do this if you are staring to learn Clojure and are happily hacking away in the Repl or evaluating code from Emacs using Slime. But, it is possible and very easy to do. You'll need to be using Leiningen so at this point I'll assume you are.
Java and One-JAR
Back in the day when you wanted to deliver your Java application you had to collect all the dependent Jar files and your own Jar or class files and then come up with a script to kick the whole thing off. You had to make sure things were all in the right place and your Classpath was correct.
Then On-JAR came along. At the time I thought this was fantastic. What it did was provide an Ant task that could package up your code with all the dependent files into a single jar file. You ran your application like so; "java -jar your-app-cmdline.jar". I used it quite often for a number of applications.
Clojure, being built on Java would have similar issues if you wanted to package things up yourself and deliver class files and Jars. Nicely, the build tool Leiningen has the 'uberjar' command. This packages your app with it's dependencies into a single stand alone jar just like One-JAR. To enable this you only need to do a few things in your project.
1. Edit project.clj
First, you need to edit your 'project.clj' file and add key :main with a value that is your main namespace.
(defproject cmdline "1.0.0" :description "Command line application in Clojure" :dependencies [[org.clojure/clojure "1.3.0"]] :main cmdline.core )
2. Edit core.clj
Next, go into the namespace file you just put in project.clj and add a gen-class form like the following.
(ns cmdline.core (:require [clojure.java.io :as io]) (:gen-class :main true))
3. Define -mainLastly, you need to define a -main function. The :gen-class will tell clojure to build a class files with a 'main' function. The -main you define will be main function.
(defn -main "The application's main function" [& args] (println args))
Build and Run
With those simple three steps completed you use Leiningen to build your single jar.
$ lein uberjar
Then you run your application as follows. Any arguments will be presented to your -main function as a list in the args variable.
$ java -jar cmdline-1.0.0-standalone.jar hello!
You'll notice that your function -main is defined to take an optional value args which will contain any arguments you sent on the command line. For simple applications you can process this list by hand. For example, suppose you expect a single value which you'll use to do something with. If this value is present you'll print out a usage statement. This can be done like so.
(defn -main "The application's main function" [& args] (if args (println (str "You passed in this value: " args)) (println "Usage: cmdline VALUE")))
For simple scenarios this works but quickly becomes unmanageable when you start thinking of flags and multiple parameters.
For more complex command line parameter scenarios there is a library called tools.cli. Available on github at ttps://github.com/clojure/tools.cli, the library supports command line parameter parsing in a simple fashion.
To use the library in your project you only need to add "[org.clojure/tools.cli "0.2.1"]" to your project file in the :dependencies section.
(defproject cmdline "1.0.0" :description "Command line application in Clojure" :dependencies [[org.clojure/clojure "1.3.0"] [org.clojure/tools.cli "0.2.1"]] :main cmdline.core )
Then you need to reference it in your namespace declaration.
(ns cmdline.core (:use [clojure.tools.cli :only (cli)]) (:gen-class :main true))
The only function you need is cli. You pass your args variable to cli along with vectors that describe the parameters you are looking for. These vectors are descriptors for each parameter the application is going to handle.
For example, if you want to support a help flag. You might want this flag to be '-h' or '--help' and if either is present have the app respond with some usage text. In this case you'll want the app to not be bothered if the help flag is not present. The vector syntax for this would be the following:
["-h" "--help" "Show help" :flag true :default false]
Another example, would be a parameter which you expect to be a number. Also, you want the app to set a value to a default value if this parameter is not present. The following is syntax for a delay parameter which if not present defaults to 2.
["-d" "--delay" "Delay between messages (seconds)" :default 2]
If you want to specify a simple value you can do that as follows.
["-f" "--from" "From address"]
With a set of these vectors you pass them along with the args value to cli which returns three vectors. The first is a options map with keys that match you specified parameter names and their associated values. The second vector is the original args value and the last is a value containing the usage text which when printed explains the parameters you've declared.
If you find a required value missing from your options map you can simply print the banner and exit.
Example For Command Line Email Application
The following is a real-world example from an application that mass mails a email to a list of recipients. Notice, that is supports a help flag, has a few defaulted optional parameters (delay and test) as well as few parameters that expect values (from, email-file, subject and message-file).
The other thing to notice is how the first parameter returns from cli, here called opts to indicate these are the options is a map and you can get the values by using the keys you described in the initial call to cli. For example, you can get the subject value with '(:subject opts)'.
The run function included here simply prints out the options and arguments passed to it.
(defn run "Print out the options and the arguments" [opts args] (println (str "Options:\n" opts "\n\n")) (println (str "Arguments:\n" args "\n\n"))) (defn -main [& args] (let [[opts args banner] (cli args ["-h" "--help" "Show help" :flag true :default false] ["-d" "--delay" "Delay between messages (seconds)" :default 2] ["-f" "--from" "REQUIRED: From address)"] ["-e" "--email-file" "REQUIRED: Email addresses FILE)"] ["-s" "--subject" "REQUIRED: Message subject"] ["-m" "--message-file" "REQUIRED: Message FILE"] ["-b" "--bcc" "BCC address"] ;; optional ["-t" "--test" "Test mode does not send" :flag true :default false] )] (when (:help opts) (println banner) (System/exit 0)) (if (and (:from opts) (:email-file opts) (:subject opts) (:message-file opts)) (do (println "") (run opts args)) (println banner))))
Try the following from the project root to see how it works.
$ lein run -d 2 -f "foo.com" -e "emails.csv" -s "Subject" -m "message.txt" Arg1 Arg2 Arg3
The GitHub repository for this example is available at http://github.com/bradlucas/cmdline.