I was browsing endlessly on Twitter and I got this tweet from Mohammed Aboullaite in my timeline:
When it comes to building a command line tool, #Java is generally not the first language that comes to mind!
— Mohammed Aboullaite (@laytoun) May 9, 2020
I spent the day playing with @picocli building a #covid19 tracker CLI, compiled to native binary using @graalvm! It was funhttps://t.co/ngLKr2JUwY
And I thought that was a great little CLI utility. Nowadays I just write these kind of things in Clojure with Babashka, so I felt like I needed to do it, it must be less than 50 lines of code.
To see it in action, it’s very like the one from Mohammed, but instead in Clojure run with Babashka.
It took me like an hour, mostly to deal with the JSON returned by the API with dates encoded in US format as dictionnary key, that needed to be sorted.
Otherwise easy peasy, the tooling is great, Clojure shines and babashka starts instantly, freaking awesome.
Why would I want to write bash anymore?
Babashka?
This is an extract from the Babashka Github repository README
A Clojure babushka for the grey areas of Bash.
Life’s too short to remember how to write Bash code. I feel liberated.
— @laheadle on Clojurians Slack
The main idea behind babashka is to leverage Clojure in places where you would be using bash otherwise.
As one user described it:
I’m quite at home in Bash most of the time, but there’s a substantial grey area of things that are too complicated to be simple in bash, but too simple to be worth writing a clj/s script for. Babashka really seems to hit the sweet spot for those cases.
Goals:
- Goals
- Low latency Clojure scripting alternative to JVM Clojure.
- Easy installation: grab the self-contained binary and run. No JVM needed.
- Familiarity and portability:
- Scripts should be compatible with JVM Clojure as much as possible
- Scripts should be platform-independent as much as possible. Babashka offers support for linux, macOS and Windows.
- Allow interop with commonly used classes like java.io.File and System
- Multi-threading support (pmap, future, core.async)
- Connectivity: talk UDP, TCP, HTTP (and optionally: JDBC)
- Support for various data formats: JSON, XML, YAML, CSV, bencode
- Batteries included (tools.cli, cheshire, …)
- Library support via popular tools like the clojure CLI
Babashka is developed by Michiel Borkent - @borkdude.
Show me the code already
#!/usr/local/bin/bb
(require '[clojure.java.shell :refer [sh]]
'[babashka.curl :as curl]
'[clojure.java.io :as io]
'[table.core :as t]
'[clojure.tools.cli :as cli])
(def cli-options
[["-c" "--country <country>" "The country to fetch COVID details for" :default nil]
["-g" "--graph" :default false]])
(defn table [detail]
(-> (dissoc detail :updated :deathsPerOneMillion :testsPerOneMillion :casesPerOneMillion :countryInfo)
vector
(t/table :style :unicode)))
(defn fetch [url]
(-> (curl/get url)
:body
(json/parse-string true)))
(defn graph [d title]
(let [data (apply str (interpose " " d))
graph (:out (sh "asciigraph" "-h" "10" "-c" title :in data))]
(println graph)))
(defn graph-values [m]
(let [transform (fn [s] (str/replace s #"(\d+)/(\d+)/(\d+)" "$3-$1-$2"))
mapping (into (sorted-map) (map #(vector % (transform %)) (keys m)))
new-map (clojure.set/rename-keys m mapping)]
(vals (into (sorted-map) new-map))))
(defn print-country-graph [c]
(let [{:keys [:cases :deaths :recovered]} (:timeline (fetch (str "https://corona.lmao.ninja/v2/historical/" c)))]
(graph (graph-values cases) "Number of cases")
(graph (graph-values deaths) "Number of deaths")
(graph (graph-values recovered) "Number of recovered")))
(let [{:keys [:country :graph]} (:options (cli/parse-opts *command-line-args* cli-options))]
(if (nil? country)
(table (fetch "https://corona.lmao.ninja/v2/all"))
(do (table (fetch (str "https://corona.lmao.ninja/v2/countries/" country)))
(when graph (print-country-graph country)))))
As expected less than 50 lines. The repository is here: @agrison/covid19-babashka.
You need the asciigraph utility installed to render line plots in ASCII, I could not use it directly from Babashka because it’s not possible to import Java classes like this. This project should have a Clojure wrapper, or maybe we can use a pod for this, I need to dive into it.
Thanks Mohammed for the idea, and Michiel for Babashka.
Until next time!