Scripting with clj

One of the features I’d love to have is scripting in Clojure. I would like to create stand-alone scripts that I can run, but are written in Clojure. When I first saw clj, I thought it might be it and actually I am using it in a couple of places; but I’m finding a number of problems in using it effectively.

  1. Shebang.
    Obviously, it would be useful to have a script that I can just run and not care what it is written in. so it should start with a #! /usr/bin/env clj - but - being said script a Clojure file, it does not work.
  2. External deps file
    I am currently living with a top deps.edn file at the root of my project, but it end up being a free-for-all where a lot of jars live. It would be better to have a way to specify which deps we need in the script itself.
  3. Scripting conveniences
    It would be nice to integrate a simpler version of so you can easily declare params in.

Maybe I’m using the wrong tool for the job, or maybe I should just write a shell script that strips the first lines starting with a #, builds an ad-hoc deps.edn and runs clj behind the scenes.

What do you think? anybody else having similar concerns?

PS. Scripts plus Sqlite end up being quite powerful for a lot of small tasks…


FWIW, you actually can start a Clojure file with #! /usr/bin/env clojure and it will work fine (additionally you don’t need to use a .clj extension on the file being run). You can also pass the contents of the deps.edn as a parameter to clojure via the -Sdeps {:deps {...}} command-line option. Unfortunately there’s no portable way to combine these two options since env doesn’t work the same way across platforms.


I’m trying to write my scripts in Clojure tool with new deps and CLI. But instead of using it globally with shebang, I prefer running projects locally based on my habits from Node.js projects.

I did that in my workflow recently by adding cli/ folder and adding it to :paths ["cli/"]. Then I can call from command line:

clj -m build.task-name

Not short enough but available for my case. Then combining with sh and I got my scripts:

(ns build.upload
  (:require [ :refer [sh]]))

(defn sh! [command]
  (println command)
  (println (sh "bash" "-c" command)))

(defn -main []
  (echo "run task")

It’s not bad but I there are something I need to become faster:

  • a shorter form of invoking functions.
  • I would like to call specific functions for each task(actually shadow-cljs can do that).
  • starts faster, always wanted. and better error messages.

I don’t use many dependencies yet. However I do want to see more tasks I can import and use in my scripts. Writing Bash scripts is a little dirty.


Clojure isn’t particularly well suited to scripting, both because of how deps are managed and the rather slow startup time. The easiest way I know of to get the kind of experience you seem to be looking for is with Boot’s scripting support. I’ve got some Clojurescript code that I compile and then run using node to generate graphs and such, but really I’m more likely to use another language for that sort of thing.


I believe Planck might be a better fit for this: It compiles and executes Clojurescript in Node, which has a much faster startup time. It also includes some utilities specifically for shell scripting.


Like others have said, boot natively supports shebang with dependency scripts. Or if you prefer lein, this is what I use:

Planck does not use Node. You are thinking of Lumo (

Planck uses JavaScriptCore instead of Node.


I have been using the #!/usr/bin/env clojure shebang line in a no-dependencies script and have been happy enough with the current startup time.

This is what I am seeing:

$ echo $'#!/usr/bin/env clojure\n(println "hello!")' > hello
$ chmod +x hello
$ time ./hello 

real    0m0.881s
user    0m1.808s
sys     0m0.060s

So now I’m curious about the startup time issue. Are those 0.88 seconds unacceptable for most people? Or does the startup time increase a lot when you start adding lots of dependencies?


Startup times increase linearly with every added require/use clause.

I do as well - startup not exactly stellar, but the convenience of using Clojure plus JDBC drivers makes it quite worthwhile.

The time takes to run Clojure alone is acceptable, given that rsyncing assets takes many seconds. If you are compiling front-end projects, it would take more than 30s. Normally I can start Clojure in less than 5s.

1 Like

Update: I am using scripting a lot lately, using clj and direct git links to our company repos for complex things/dependencies. It’s not real quick, but from the command line is more than acceptable on an old MBP.
I lately tackled the parameter passing issue, so I get scripts that are self-documenting and I can just call them without parameters to know what they are supposed to be doing.

I wonder if an env-wrapper like could work?

I usually keeps scripts under src, so to launch a script in foo.clj, I use:

     clj -m foo report --from 2017-01-01 --to 2018-01-01

and it just works. It would be cooler if it was just foo, but you get used to it.

I’m 100% sure that if you change the word “scripting” for “automation” you’ll get better answers.

1 Like

On windows .net, Clojure-clr is suitable for writing scripts as a better powershell. You can just put the dependencies in the same directory as clojure.main.exe.

I ended up using shadow-cljs and node.js. It’s not the JVM but still.

Shadow-cljs is really good at importing npm packages and the dev experience is really really good (hot reload etc…). My scripts are in clojurescript and at the end of the day I have a js file that starts-up really fast and does the job.

1 Like

Check out this boilerplate snippet from Eric Normand. The tl;dr

  #_DEPS is same format as deps.edn. Multiline is okay.
  DEPS='{:deps {clj-time {:mvn/version "0.14.2"}}}'

  #_You can put other options here
  OPTS='-J-Xms256m -J-Xmx256m -J-client'

  exec clojure $OPTS -Sdeps "$DEPS" "$0" "[email protected]"
;; Clojure code....

It looks a bit like cheating :slight_smile: but I’m sure it can be useful. I’ll steal it!

1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.