Skip to content

Instantly share code, notes, and snippets.

@ericnormand
Last active January 6, 2024 07:13
Show Gist options
  • Star 86 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save ericnormand/6bb4562c4bc578ef223182e3bb1e72c5 to your computer and use it in GitHub Desktop.
Save ericnormand/6bb4562c4bc578ef223182e3bb1e72c5 to your computer and use it in GitHub Desktop.
Boilerplate for running Clojure as a shebang script
#!/bin/sh
#_(
#_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" "$@"
)
(println "Hello!")
(require '[clj-time.core :as t])
(prn (str (t/now)))
(prn *command-line-args*)
(println (.. (Runtime/getRuntime)
totalMemory))
$ cp script.clj ~/bin/cljtest2
# ~/bin is on my $PATH
$ chmod +x ~/bin/cljtest2
$ time cljtest2 "Yo" "Hey" 1 3 4 - -ff
Hello!
"2019-03-01T17:22:43.564Z"
("Yo" "Hey" "1" "3" "4" "-" "-ff")
268435456
real 0m2.073s
user 0m6.073s
sys 0m0.297s
$
@chrisbetz
Copy link

I love this.

Strong odds that someone with better shell-fu than me could reads the deps directly from the file!

If you are willing to rely on bash (or zsh), you could use

DEPS=$(<deps.edn)

instead.

@kevinjamescasey
Copy link

That is cool. Thanks for the explanations!

So you have to be a little careful about what you put between the parens on lines 2 and 16. For example I had to add quotes around the URL in this line to make the Clojure reader happy:

   #_this shebang genius is from "https://gist.github.com/ericnormand/6bb4562c4bc578ef223182e3bb1e72c5"

@piranha
Copy link

piranha commented Dec 22, 2022

Just in case somebody is looking at this and is okay with having separate deps.edn (I do, mainly for cider-jack-in-clj), then this could be simplified to a single string:

":";exec clojure -M -m $(basename $0 .clj)

This should be a first line of a file. From Clojure's point of view, it's a string ":" and then a comment (since it starts with ;), so a no-op practically.

From shell's point of view, if there is no shebang and no pound sign as a first character of a file, then this file is going to be run with /bin/sh. : is an empty command (try it out in your shell), then ; is a command separator and then next command is exec effectively handing control over to clojure.

All this stuff here is to pass -m namespace to clojure, so that it will call function -main in that script. This means you can safely eval this in REPL how many times you want without re-executing initialization code. This is done by $(basename $0 .clj), of course.

Babashka sets "babashka.file" system property to a file being run, which simplifies all that machinery. clojure does nothing similar, so this is the way I came up with. Would love to find out if there is a way like Python's if __name__ == "__main__".

@yogsototh
Copy link

yogsototh commented Apr 24, 2023

I just figured out a way to make it even more portable by adding a step that install a specific version of Clojure local to the script.

#!/bin/sh
#_(

   #_DEPS is same format as deps.edn. Multiline is okay.
   DEPS='
   {:deps {
           clj-http/clj-http {:mvn/version "3.12.3"}
           cheshire/cheshire {:mvn/version "5.11.0"}
           }}
   '

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

if [[ ! -x .local/bin/clojure ]]; then
  [[ ! -d .local ]] && mkdir .local
  pushd .local
  curl -O https://download.clojure.org/install/posix-install-1.11.1.1273.sh
  chmod +x posix-install-1.11.1.1273.sh
  ./posix-install-1.11.1.1273.sh -p $PWD
  popd
fi

exec .local/bin/clojure $OPTS -Sdeps "$DEPS" "$0" "$@"
)

(require 
  '[clj-http.client :as client]
  '[clojure.pprint :as pp])

(defn -main [& args]
  (pp/pprint (:body (client/get "https://www.example.com" {}))))

(apply -main *command-line-args*)

So with this small block added, your only dependency is java.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment