Thanks to the recent developments from the ClojureScript community, writing command-line scripts in Clojure has been a fun experience for me. Major kudos to @anmonteiro for developing lumo and the core ClojureScript team.
I think Clojure is a great language for writing single-use scripts to process data because of the built-in manipulation functions and immutable structures so you won’t have to worry about references and deep-cloning.
Running scripts with Lumo
The easiest way to get started is to install lumo
on your system and run Clojure files with it (see NPM section below if you don’t want to install lumo
globally). Create a file called hello.cljs
with the following contents:
(println "Hello World!")
and run it using:
$ npm i -g lumo-cljs ## or other package manager of your choice
$ lumo hello.cljs
Hello World!
Couldn’t be easier. Now let’s look at how we can leverage Node.JS APIs to write a slightly more practical program:
As you can see, the require
function can now act like its JS counterpart and pull in JS modules as you would with the following code:
const { writeFileSync } = require('fs');
const inputJson = require('./randomUsers.json');
It also works with modules inside the node_modules
folder installed using npm
in the same directory (as of lumo 1.8.0
). U̵n̵f̵o̵r̵t̵u̵n̵a̵t̵e̵l̵y̵,̵ ̵t̵h̵e̵r̵e̵ ̵i̵s̵n̵’̵t̵ ̵a̵n̵ ̵e̵a̵s̵y̵ ̵w̵a̵y̵ ̵t̵o̵ ̵m̵a̵n̵a̵g̵e̵ ̵d̵e̵p̵e̵n̵d̵e̵n̵c̵i̵e̵s̵ ̵o̵n̵ ̵t̵h̵e̵ ̵C̵l̵o̵j̵u̵r̵e̵ ̵s̵i̵d̵e̵ ̵j̵u̵s̵t̵ ̵y̵e̵t̵ ̵s̵o̵ ̵f̵o̵r̵ ̵n̵o̵w̵ ̵w̵e̵ ̵a̵r̵e̵ ̵s̵t̵u̵c̵k̵ ̵w̵i̵t̵h̵ ̵m̵a̵n̵u̵a̵l̵l̵y̵ ̵h̵a̵n̵d̵l̵i̵n̵g̵ ̵J̵A̵R̵ ̵f̵i̵l̵e̵s̵ ̵w̵h̵e̵r̵e̵ ̵C̵l̵o̵j̵u̵r̵e̵ ̵l̵i̵b̵r̵a̵r̵i̵e̵s̵ ̵a̵r̵e̵ ̵u̵s̵u̵a̵l̵l̵y̵ ̵p̵a̵c̵k̵a̵g̵e̵d̵.̵ ̵(̵T̵a̵k̵e̵ ̵a̵ ̵l̵o̵o̵k̵ ̵a̵t̵ ̵t̵h̵e̵ ̵l̵u̵m̵o̵ ̵w̵i̵k̵i̵ ̵f̵o̵r̵ ̵m̵o̵r̵e̵ ̵d̵e̵t̵a̵i̵l̵s̵.̵)̵
Update 2018 Oct: I just found out that it’s possible to use Clojure’s CLI tool clj
to manage and set the classpath for Clojure-side dependencies via deps.edn
(https://clojure.org/guides/deps_and_cli). In the demo project below, simply replace the lumo flag -c src
with -c `clj -Spath`
and you should be able to require all the dependencies specified in your deps.edn
file.
Integrating with NPM
For slightly larger projects you’d probably want to have a proper package.json
with some dependencies on NPM. You could also install lumo
on a per-project basis if you don’t want to install it globally or wish to publish the package elsewhere. Here’s a sample project that works like the above example but fetches JSON from https://randomuser.me using the request
library from NPM and splits the code into 2 Clojure files with proper name-spacing:
my-tool
|\_ package.json
\_ src
\_ my_tool
|\_ core.cljs
\_ user.cljs
Clojure’s namespace system mirrors the directory structure so the file with the ns my-tool.core
must be my_tool/core.cljs
.
Gotcha: Hyphen delimited names in the namespace MUST be converted into snake_case in the filesystem.
core.cljs
:
user.cljs
:
package.json
:
The -c
flag tells lumo where your source files are and the -m
flag specifies which namespace your -main
function is in. You can run this tool using the usual npm procedures:
$ npm install
$ npm start 12 ## fetches 12 users and outputs randomUsers.edn
REPL Development
Of course, no Clojure experience would be complete without interactive REPL-based development. Add the following line into the scripts
section of your package.json
file:
"repl": "lumo -c src -i src/my_tool/core.cljs -n 5777 -r"
The -i
flag initializes the REPL with our entry point core.cljs
, the -n
flag starts a socket REPL on port 5777 for editor integration and finally the -r
flag start a REPL in the terminal. With this you could execute arbitrary code in your runtime without loosing state:
$ npm run repl
...
cljs.user=> (in-ns 'my-tool.core) ;; switch to our core namespace
my-tool.core=> (user/parse {:name {:first "john" :last "smith"}})
{:id "c1b61773-133e-434c-afbd-d82b95b814d3",
:username nil,
:password nil,
:email nil,
:full-name "John Smith"}
Final Thoughts
While there are still a few rough edges with the current tooling, I think this is the time where writing CLI scripts in Clojure starts to become a viable option and a nice alternative to JS.
Lumo’s startup time is blazing fast compared to any clojur-y things running on the JVM so it’s a breath of fresh air.
I would definitely recommend trying this out if you enjoy Clojure.