Lumo: Brightening the Horizons for Clojurescript'ing
Introduction
Clojurescript'ing just got a lot easier.
Lumo is a standalone clojurescript compiler developed by António Nuno Monteiro. It is built on top of the NodeJS ecosystem, and the first version of Lumo was introduced November 2016 with unanimous acclaim. This tool allows clojurescript to target backend development, especially in areas where clojure is lackluster.
In this blog post, I will be using Lumo for a specific use case, to write simple and re-useable commandline scripts. Get your finger-guns ready, because you're in for a wild-ride!
Lets Get Started (Prerequisites)
Lumo requires you to have a node development environment setup on your system. You can set this up using your OS's package manager, or obtain further instructions on how to get this setup from the Official Website.
If you're using Ubuntu, you can install npm
using apt-get
:
$ sudo apt-get install npm
If everything went according to plan, you should have npm
installed:
$ npm --version
3.5.2
Installing Lumo
I'm going to grab a copy of Lumo using npm
:
npm install -g lumo-cljs
Note that the installed binary should be called lumo
instead of lumo-cljs
.
I'm going to check to make sure lumo
is installed:
$ lumo --version
1.8.0
Looking good.
One Liners
lumo
supports one-liners by use of the -e
, –eval
flag:
$ lumo -e "(* Math/PI 2)"
6.283185307179586
$ LUMO_VERSION=$(lumo --version)
$ lumo -e "(as-> \"$LUMO_VERSION\" $ (re-matches #\"([0-9]+)\.([0-9]+)\.([0-9]+)\" $) (nth $ 2) (println $))"
8
But to truly leverage scripting, it would be more remarkable to write clojurescript commandline programs.
Scripting Methods
There are several ways to work with Lumo, and some methods are more suitable based on the OS you are using Lumo from. The first example should work on any OS platform.
The Beginnings
The easiest way to construct a script is to create a new file with the clojurescript extension .cljs and run it directly with the lumo
commandline tool.
I'm going to create a hello.cljs file with the following contents:
(println "Hello World!")
Then, from the commandline:
$ lumo hello.cljs
Hello World!
Not Scripty Enough...
In order for Lumo to be used for scripting, it needs to support the shell scripting execution idioms, and fortunately, it does alright.
Note that up to this point, you could happily follow along on a Windows machine. Certain functionality here is not available to Windows users for now. In the future, some of this functionality will be present with upcoming Windows 10 updates, which appear to be incorporating *nix scripting idioms.
I'm going to create a new file called hello which contains the same contents as our previous script file, except for an additional line included at the top of the file:
#!/usr/bin/env lumo
(println "Hello World!")
If you're familiar with bash scripting, that extra line probably looks like something you've seen before. Next I'm going to make it so that I can execute this file:
$ chmod 744 hello
Executing this script file should return the same result as our clojurescript file:
$ hello
Hello World!
This is cool and all, but there is still a lot to cover.
Commandline Arguments
Commandline arguments are accessed through the var *command-line-args*
. I'm going to modify the hello script to accept the first commandline argument as the person we are saying 'hello' to:
#!/usr/bin/env lumo
(if-let [name (first *command-line-args*)]
(println (str "Hello " name "!"))
(println "Hello World!"))
Running this:
$ hello
Hello World!
$ hello Ben
Hello Ben!
Node Standard Libraries
Since Lumo is built as an executable targeting the Node ecosystem, it means we can leverage everything the Node API has to offer. If you're interested in what is available to you out-of-the-box, all of the documentation on the Node API can be found here. I'm going to try writing a script called listdir which lists the current directory, or lists the files for a path provided as a commandline argument. All of this is done using the File System API.
#!/usr/bin/env lumo
(def fs (js/require "fs"))
(defn real-path [path]
(.realpathSync fs path))
(defn list-dir [path]
(.readdirSync fs path))
(let [p (or (first *command-line-args*) ".")
dir-listing (-> p real-path list-dir)]
(doseq [fd dir-listing]
(println fd)
))
Running this script results in a less feature complete ls
:
$ chmod 744 listdir
$ listdir
hello
hello.cljs
listdir
$ listdir ~/.mozilla
extensions
firefox
systemextensionsdev
External Node Dependencies
Being able to use the Node API is great and all, but it would be spectacular if you could also use community-made libraries.
I'm always aching to make use of the infamous left-pad library. It would be great if it were a commandline tool called leftpad:
#!/usr/bin/env lumo
(def left-pad (js/require "left-pad"))
(def help-msg "leftpad <text> <num> [padding]")
(defn leftpad
([] help-msg)
([_] help-msg)
([text num] (left-pad text num))
([text num padding] (left-pad text num padding))
([text num padding & _] help-msg))
(println (apply leftpad *command-line-args*))
When I attempt to run this script:
$ chmod 744 leftpad
$ leftpad
Cannot find module 'left-pad'
Function.Module._resolveFilename (module.cljs:542:15)
Function.Module._load (module.cljs:472:25)
Module.require (module.cljs:585:17)
require (internal/module.cljs:11:18)
Object.<anonymous> (/home/benzap/.bin/leftpad:1:84)
Module._compile (module.cljs:641:30)
(Object._t)
(Object.lumo.repl.caching_node_eval)
(NO_SOURCE_FILE <embedded>:6127:273)
E (NO_SOURCE_FILE <embedded>:6128:269)
Hey... What Happened?
Similar to Node, Lumo attempts to find the dependencies in a sub-folder called node_modules. I can pull in the left-pad dependency with:
npm install left-pad
Naturally upon calling this command, npm
will complain about a missing package.json file, and cuss you out for not having all of the bells and whistles for developing your program. All that i'm interested in is the generated node_modules sub-folder containing the precious left-pad payload:
$ listdir
hello
hello.cljs
leftpad
listdir
node_modules
$ listdir ./node_modules
left-pad
So as you can see, we have a sub-folder called node_modules, which contains our node dependency left-pad. Running the leftpad commandline tool as before should give us the correct output:
$ leftpad
leftpad <text> <num> [padding]
$ leftpad Bacon
leftpad <text> <num> [padding]
$ leftpad Bacon 10
Bacon
$ leftpad Bacon 10 -
-----Bacon
$ leftpad Bacon 10 - yum
leftpad <text> <num> [padding]
Write once, Execute anywhere
Up to this point, I've been placing all of these scripts in a sub-folder in my home directory at ~/.bin
. I would love to just start using these scripts where ever I need to leftpad a string, or list directories. I'm going to go ahead and include this folder in my PATH environment variable, so I can use these commands anywhere:
$ printf "export PATH\nPATH=\$PATH:~/.bin\n" >> ~/.bashrc
$ cd
$ source .bashrc
$ listdir ~/.bin
hello
hello.cljs
leftpad
listdir
node_modules
$ leftpad Hello 10 -
-----Hello
TL;DR
In this blog post, I covered:
- How to become a Clojurescript'ing cowboy
- Writing your first Clojurescript script with commandline arguments
- Writing a Clojurescript script with the Standard Node API
- Writing a Clojurescript script with External Node Dependencies
There's a snake in my boot!
If you feel like I've missed something, or you wish to challenge me to a duel, you can find me on the clojurians slack as @benzap.