Enjoy scripting with Clojure

2016-08-01

Every day we write crappy little scripts to shuffle files between directories, download/upload something from an API, generate PDFs, and other miscellanea.

Most folks I know usually reach for Ruby or Python for this kind of thing.

Me? I like Clojure.

Why? Aside from the usual reasons ("robust, practical, and fast") and Just Really Liking Clojure, Boot has made this kind of "shitty little scripting" really pleasant.

Getting boot is just brew install boot-clj.

Here's how I do it:

$ clj-new-script a_great_script

$ cat a_great_script

#!/usr/bin/env boot

(set-env! :dependencies '[[org.clojure/data.csv "0.1.3"]

                          [me.raynes/fs "1.4.6"]
                          [instaparse "1.4.2"]]) 
(require '[clojure.spec :as s]
         '[clojure.java.io :as io]
         '[clojure.java.shell :as sh]
         '[clojure.data.csv :as csv]
         '[me.raynes.fs :as fs]
         '[instaparse.core :as insta]) 
(import '[java.time Instant])

(defn -main [& args]
(println "hello, world"))

This is 16 tiny lines, but let's break it down anyway.

First, we declare a shebang that calls boot:

#!/usr/bin/env boot

This, plus chmod +x scriptname is all it takes to run a valid Clojure script in the familiar ./scriptname style.

Next, we declare the external libraries our script depends on.

In this case I pull in Clojure Contrib's data.csv for CSV parsing/generating, Raynes' fs for handy filesystem interaction, and Engelberg's awesome instaparse library for parsing context-free grammars:

(set-env! :dependencies '[[org.clojure/data.csv "0.1.3"]
                          [me.raynes/fs "1.4.6"]
                          [instaparse "1.4.2"]]) 

Boot will resolve these dependencies the first time we run the script and cache them from then on.

Next, we link our script against some namespaces and Java classes:

(require '[clojure.spec :as s]
         '[clojure.java.io :as io]
         '[clojure.java.shell :as sh]
         '[clojure.data.csv :as csv]
         '[me.raynes.fs :as fs]
         '[instaparse.core :as insta]) 
(import '[java.time Instant])

The -main fn accepts args as you think it would:

(defn -main [& args] 
(println "hello, world"))

This -main fn is sparse, but it would be easy enough to add in validation with clojure.spec.

It really is this simple: declare a shebang, some dependencies, links, a main fn, and you're on your way.

I would put this scripting workflow up against Ruby/Python/Bash any day for its brevity, simplicity, functionality (automatic inline dependency resolution!), and speed.

Notes

  • The Boot Scripting guide can be found here. It outlines a few additional scripting features Boot provides.
  • For those who would rather do their scripting with Clojurescript via Node or JavaScriptCore over the JVM, there is the phenomenal Planck which is similarly straightforward to the approach outlined above.
  • For those partial to Scala, I have used Li Haoyi's Ammonite in production, and it is unparalleled for this kind of work. Highly recommended.
  • The source for clj-new-script is here.