Shell Scripting in Clojure with Pallet

Let's face it, many of us hate writing shell scripts, and with good reason. Personally, it's not so much the shell language itself that puts me off, but organising everything around it; how do you deploy your scripts, how do you arrange to call other scripts, how do you manage the dependencies between your scripts? Pallet aims to solve these problems by embedding shell script in clojure.

Embedding in Clojure

Embedding other languages in lisp is not a new idea; parenscript, scriptjure (which Pallet's embedding is based on), and ClojureQL all do this.

So what does shell script in clojure look like? Some examples:

(script
  (ls "/some/path")
  (defvar x 1)
  (println @x)
  (defn foo [x] (ls @x))
  (foo 1)
  (if (= a a)
    (println "Reassuring")
    (println "Ooops"))
  (println "I am" @(whomai))

which generates:

ls /some/path
x=1
echo ${x}
function foo(x) {
ls ${x}
 }
foo 1
if [ \\( \"a\" == \"a\" \\) ]; then echo Reassuring;else echo Ooops;fi
echo I am $(whomai)

The aim is to make writing shell script similar to writing Clojure, but there are obvious differences in the language that limit how far that can be taken. To run the code above at the REPL, you'll have to use the pallet.stevedore package.

Escaping back to Clojure

Escaping allows us to embed Clojure values and expressions inside our scripts, in much the same way as symbols can be unquoted when writing macros.

(let [path "/some/path"]
  (script
    (ls ~path)
    (ls ~(.replace path "some" "other)))

We can now write Clojure functions that produce shell scripts. Writing scripts as clojure functions allows you to use the Clojure namespace facilities, and allows you to distribute you scripts in jar files (which can be deployed in a versioned manner with maven)

(defn list-path [path]
  (script
    (ls ~path)
    (ls ~(.replace path "some" "other)))

Composing scripts

Pallet allows the scripts to be combined. do-script concatenates the code pieces together.

(do-script
  (list-path "path1")
  (list-path "path2"))

chain-script chains the scripts together with '&&'.

(chain-script
  (list-path "path1")
  (list-path "path2"))

checked-script finally allows the chaining of scripts, and calls exit if the chain fails.

(checked-script "Message"
  (list-path "path1")
  (list-path "path2"))

Conclusion

Writing shell script in Clojure gives access to Clojure's namespace facility allowing modularised shell script, and to Clojure's packaging as jar files, which allows reuse and distribution. The ability to compose script fragments leads to being able to macro-like functions, such checked-script, and you could even use Clojure macros to generate script (but I haven't thought of a use for that, yet).

The syntax for the embedding has arisen out of practical usage, so is far from complete, and can definitely be improved. I look forward to hearing your feedback!

UPDATE: stevedore now requires a binding for *template*, to specify the target for the script generation. This should be a vector containing one of :ubuntu, :centos, or :darwin, and one of :aptitude, :yum, :brew.