Tablizer: fun experiment with Clojure CLI tools

I just created Tablizer, it’s a little command line tool that takes a URL, fetches the page, finds any <table> tags, and prints them out as ASCII table (org-mode style).

I first started with lein new tablizer, but then decided against that and instead just created a deps.edn and took it from there. I have the say: the experience was great!

You can just put a shebang on top of the file, #!/usr/bin/env clj, make a Clojure file executable, and bam, you got yourself a command line script. To be honest I didn’t really expect the shebang to work but tried it anyway. Having this “it’s just UNIX” workflow is IMO a big selling point, as people can try out the language using a workflow they are familiar with.

To be able to use CIDER while working on the script I added a bin/cider shell script, which loads bin/cider.clj. All together it looks like this

;; deps.edn
 {org.clojure/clojure {:mvn/version "1.9.0"}
  org.slf4j/slf4j-nop {:mvn/version "1.7.25"}
  sparkledriver {:mvn/version "0.2.2"}}

 {:nrepl {:extra-deps {org.clojure/tools.nrepl {:mvn/version "0.2.12"}}}
  :cider {:extra-deps {cider/cider-nrepl {:mvn/version "0.15.1"}}}}}
#### bin/cider
clj -R:nrepl:cider bin/cider.clj $* &
sleep 10
emacsclient -e '(cider-connect "localhost" "'${1:-7888}'" "'`pwd`'")'
;; bin/cider.clj
(require '[ :refer [start-server]]
         '[cider.nrepl :refer [cider-nrepl-handler]])

(let [port (or (some-> (first *command-line-args*)
  (start-server :port port :handler cider-nrepl-handler)
  (println "Started nREPL on port" port))

It would have been nice to just have the cider.clj script, but /usr/bin/env won’t let you pass parameters to clj. I could have made it a single script with a heredoc or something but splitting it this way seemed less messy.

It’s a little bit of boiler plate, but I think it’s really great to have this kind of “nothing up the sleeves” way of doing things, without all the magic of Lein or boot. It’s not for every occasion, but it’s great as a starting point for learning, for small CLI projects like this one, or if you just want to have that bit of extra control over your setup.


Since you mentioned shebang, I think it’s not enough to match Node.js . The different it dependencies. To run a script with dependencies loaded, one creates index.js, package.json with dependencies and bin entires, and run npm install -g . to add it globally. I don’t see clj can to that in a easy way.

Pesonally, I don’t like global install, since it make the environment dirty and hard to reproduce the development environment across machines. The “magic” of global install are quite easy to achieve by having little bash tricks.

add two lines to ~/.bash_profile

# where you store the scripts
export DEPS_HOME=~/.clj
# add the script path to executable lookup path
export PATH=$PATH:$DEPS_HOME/bin

so that bash now know where to find your executable.

little magic install script, so that we could do something like clj-install <project-name> <executable>

save it to ~/.clj/bin/clj-install


# copy deps.edn and clj file to ~/.clj
# link the script to executable search path ~/.clj/bin
# make symbolic link executable
chmod 755 $DEPS_HOME/bin/$EXEC_NAME
# now executable
chmod 755 ~/.clj/bin/clj-install
# let's try something
mkdir script-test/ && cd script-test
# add some content
vim deps.edn
vim index.clj
# install globally
clj-install script-test index.clj
# test it
which index.clj
# ~/.clj/bin/index.clj
# Hello world?

It is simple and could be used on anything which have bash…


Interesting. Then you have two scripts, which requires two deps.edn files. How about that?

Don’t really understand your question. The install script is an illustration of how install scripts works. Actually, most of the tools like npm did the same underhood. Yes, they did a lot more to make the script cross platform…more stable…fewer crash…less buggy. But the concept is as simple as 1) add a Bash search path, 2) save the script to somewhere, 3) soft link the executable to bash search path. 4) add a command to hide all the steps from user

The fs layout might let you get more understand how thing work.

----index.clj ---> ~\.clj\script-test
----index2.clj ---> ~\.clj\another-test

if we want it more general

----script-test ----> ~/.clj/repository/script-test/index.clj
----script-test2 ----> ~/.clj/repository/script-test2/index.clj
------ deps.edn
------ index.clj
------ deps.edn
------ index.clj

Solved the problem I mentioned by using folders.

That’s very nice, thanks for working out the minimal setup to work with cider! I was wondering whether there is a way to embed deps into the main Clojure code somehow - that would allow us to distribute a short Clojure util as a single file. I looked into it but there doesn’t seem to be an obvious way to do it, since the CLI scripts seem to do dependency resolution (and caching of the classpath) and running of the actual code as a two-step process.

clj has a different scope than npm, comparing the two is comparing apples and oranges. I also doubt we’ll start seeing a proliferation of command line tools based on Clojure now, there are better languages/environments for that. Still if you want to create and distribute Clojure-based command line tools I can see several options.

  • Do what I did, just let people git clone your repo which contains deps.clj and any scripts
  • Use npm. Why not? It actually contains a mechanism for adding binaries to the shell $PATH, so you can leverage that. Shadow-cljs does this, it really is a brilliant way to make people think that somehow there’s no JVM involved :slight_smile: You can bundle your deps.clj plus a wrapper script that cd's to the right directory and invokes clj . You can even bundle the whole clojure CLI tools if you want to be batteries-included.
  • Publish a jar with a :main namespace to Clojars

This last option is what I would probably do. It works with lein, boot, and clj, and extra benefit: you can also just use it as a library. install instructions are as easy as:

  • add a new alias to ~/.clojure/deps.edn
 {:mytool {:extra-deps {mytool {:mvn/version "1.0.0"}}}}}
  • add an alias to ~/.bash_profile
alias mytool='clj -R:mytool -m mytool.main'

That last option sounds particularly appealing, pretty minimal overhead in terms of “installation” and quite clean solution overall, thanks! About Clojure command line tools: I agree other technologies are better positioned for command line tools, but I think it’s more circumstantial rather than a inherent limitation of Clojure. Also, I was flirting with the idea of developing command line tools with Lumo, but that would involve diving into the node.js ecosystem to some extent, and I don’t think I was ready for that! :smiley:

I know what you mean about Lumo. I’ve already had a handful of times where I needed a little script and was like “I know, I’ll use lumo”. Half a day of wrestling with promises later I’m ready to give up and use anything else.