Michiel Borkent
Welcome reader! This is a book about scripting with Clojure and babashka. Clojure(https://www.clojure.org) is a functional, dynamic programming language from the Lisp family which runs on the JVM. Babashka is a scripting environment made with Clojure, compiled to native with GraalVM(https://www.graalvm.org). The primary benefits of using babashka for scripting compared to the JVM are fast startup time and low memory consumption. Babashka comes with batteries included and packs libraries like clojure.tools.cli for parsing command line arguments and
cheshire for working with JSON. Moreover, it can be installed just by downloading a self-contained binary.
Babashka is written for developers who are familiar with Clojure on the JVM. This book assumes familiarity with Clojure and is not a Clojure tutorial. If you aren’t that familiar with Clojure but you’re curious to learn, check out this
(https://gist.github.com/yogthos/be323be0361c589570a6da4ccc85f58f) list of beginner resources.
Babashka uses SCI(https://github.com/babashka/SCI) for interpreting Clojure. SCI implements a substantial subset of Clojure. Interpreting code is in general not as performant as executing compiled code. If your script takes more than a few seconds to run or has lots of loops, Clojure on the JVM may be a better fit, as the performance on JVM is going to outweigh its startup time penalty. Read more about the differences with Clojure here.
Installing babashka is as simple as downloading the binary for your platform and placing it on your path. Pre-built binaries are provided on the releases(https://github.com/borkdude/babashka/releases) page of babashka’s Github repo
(https://github.com/borkdude/babashka). Babashka is also available in various package managers like brew for macOS and linux and scoop for Windows. See here(https://github.com/borkdude/babashka#installation) for details.
If you would rather build babashka from source, download a copy of GraalVM and set the GRAALVM_HOME environment variable. Also make sure you have lein(https://leiningen.org) installed. Then run:
See the babashka build.md(https://github.com/borkdude/babashka/blob/master/doc/build.md) page for details.
The babashka executable is called bb. You can either provide it with a Clojure expression directly:
or run a script:
script.clj
The -e flag is optional when the argument starts with a paren. In that case babashka will treat it automatically as an expression:
Similarly, the -f flag is optional when the argument is a filename:
Commonly, scripts have shebangs so you can invoke them with their filename only:
script.clj
$ git clone https://github.com/borkdude/babashka --recursive
$ script/uberjar && script/compile
$ bb -e '(+ 1 2 3)' 6
CLOJURE
(println (+ 1 2 3)) CLOJURE
$ bb -f script.clj 6
CLOJURE
$ bb '(+ 1 2 3)' 6
CLOJURE
$ bb script.clj 6
CLOJURE
#!/usr/bin/env bb (println (+ 1 2 3))
CLOJURE
Typing bb help from the command line will print all the available command line options which should give you a sense of the available features in babashka.
Babashka v1.3.179
Usage: bb [svm-opts] [global-opts] [eval opts] [cmdline args]
or: bb [svm-opts] [global-opts] file [cmdline args]
or: bb [svm-opts] [global-opts] task [cmdline args]
or: bb [svm-opts] [global-opts] subcommand [subcommand opts] [cmdline args]
Substrate VM opts:
-Xmx<size>[g|G|m|M|k|K] Set a maximum heap size (e.g. -Xmx256M to limit the heap to 256MB).
-XX:PrintFlags= Print all Substrate VM options.
Global opts:
-cp, --classpath Classpath to use. Overrides bb.edn classpath.
--debug Print debug information and internal stacktrace in case of exception.
--init <file> Load file after any preloads and prior to evaluation/subcommands.
--config <file> Replace bb.edn with file. Defaults to bb.edn adjacent to invoked file or bb.edn in current dir. Relative paths are resolved relative to bb.edn.
--deps-root <dir> Treat dir as root of relative paths in config.
--prn Print result via clojure.core/prn
-Sforce Force recalculation of the classpath (don't use the cache) -Sdeps Deps data to use as the last deps file to be merged -f, --file <path> Run file
--jar <path> Run uberjar Help:
help, -h or -? Print this help text.
version Print the current version of babashka.
describe Print an EDN map with information about this version of babashka.
doc <var|ns> Print docstring of var or namespace. Requires namespace if necessary.
Evaluation:
-e, --eval <expr> Evaluate an expression.
-m, --main <ns|var> Call the -main function from a namespace or call a fully qualified var.
-x, --exec <var> Call the fully qualified var. Args are parsed by babashka CLI.
REPL:
repl Start REPL. Use rlwrap for history.
socket-repl [addr] Start a socket REPL. Address defaults to localhost:1666.
nrepl-server [addr] Start nREPL server. Address defaults to localhost:1667.
Tasks:
tasks Print list of available tasks.
run <task> Run task. See run --help for more details.
Clojure:
clojure [args...] Invokes clojure. Takes same args as the official clojure CLI.
Packaging:
uberscript <file> [eval-opt] Collect all required namespaces from the classpath into a single file. Accepts additional eval opts, like `-m`.
uberjar <jar> [eval-opt] Similar to uberscript but creates jar file.
prepare Download deps & pods defined in bb.edn and cache their metadata. Only an optimization, this will happen on demand when needed.
In- and output flags (only to be used with -e one-liners):
-i Bind *input* to a lazy seq of lines from stdin.
-I Bind *input* to a lazy seq of EDN values from stdin.
-o Write lines to stdout.
Scripts may be executed from a file using -f or --file:
The file may also be passed directly, without -f:
Using bb with a shebang also works:
If /usr/bin/env doesn’t work for you, you can use the following workaround:
-O Write EDN values to stdout.
--stream Stream over lines or EDN values from stdin. Combined with -i or -I *input* becomes a single value per iteration.
Tooling:
print-deps [--format <deps | classpath>]: prints a deps.edn map or classpath with built-in deps and deps from bb.edn.
File names take precedence over subcommand names.
Remaining arguments are bound to *command-line-args*.
Use -- to separate script command line args from bb command line args.
When no eval opts or subcommand is provided, the implicit subcommand is repl.
bb -f download_html.clj BASH
bb download_html.clj BASH
#!/usr/bin/env bb
(require '[babashka.http-client :as http])
(defn get-url [url]
(println "Downloading url:" url) (http/get url))
(defn write-html [file html]
(println "Writing file:" file) (spit file html))
(let [[url file] *command-line-args*]
(when (or (empty? url) (empty? file)) (println "Usage: <url> <file>") (System/exit 1))
(write-html file (:body (get-url url))))
CLOJURE
$ ./download_html.clj Usage: <url> <file>
$ ./download_html.clj https://www.clojure.org /tmp/clojure.org.html Downloading url: https://www.clojure.org
Writing file: /tmp/clojure.org.html
BASH
BASH
The var *file* contains the full path of the file that is currently being executed:
Command-line arguments can be retrieved using *command-line-args*. If you want to parse command line arguments, you can use the built-in babashka.cli (https://github.com/babashka/cli) namespace:
Note that clojure.tools.cli(https://github.com/clojure/tools.cli) is also built-in to babashka.
It is recommended to use bb.edn to control what directories and libraries are included on babashka’s classpath. See Project setup
If you want a lower level to control babashka’s classpath, without the usage of bb.edn you can use the --classpath option that will override the classpath. Say we have a file script/my/namespace.clj:
Now we can execute this main function with:
$ cat script.clj
#!/bin/sh
#_(
"exec" "bb" "$0" hello "$@"
)
(prn *command-line-args*)
./script.clj 1 2 3 ("hello" "1" "2" "3")
$ cat example.clj (prn *file*)
$ bb example.clj
"/Users/borkdude/example.clj"
BASH
(require '[babashka.cli :as cli])
(def cli-options {:port {:default 80 :coerce :long}
:help {:coerce :boolean}
(prn (:options (cli/parse-opts *command-line-args* {:spec cli-options})))
CLOJURE
$ bb script.clj {:port 80}
$ bb script.clj --port 1223 {:port 1223}
$ bb script.clj --help {:port 80, :help true}
BASH
(ns my.namespace) (defn -main [& args]
(apply println "Hello from my namespace!" args))
CLOJURE
If you have a larger script with a classic Clojure project layout like
then you can tell babashka to include both the src and test folders in the classpath and start a socket REPL by running:
If there is no --classpath argument, the BABASHKA_CLASSPATH environment variable will be used. If that variable isn’t set either, babashka will use :deps and :paths from bb.edn.
Also see the babashka.classpath namespace which allows dynamically adding to the classpath.
The namespace babashka.deps integrates tools.deps(https://github.com/clojure/tools.deps.alpha) with babashka and allows you to set the classpath using a deps.edn map.
A main function can be invoked with -m or --main like shown above. When given the argument foo.bar, the namespace foo.bar will be required and the function foo.bar/-main will be called with command line arguments as strings.
Since babashka 0.3.1 you may pass a fully qualified symbol to -m:
so you can execute any function as a main function, as long as it accepts the number of provided arguments.
When invoking bb with a main function, the expression (System/getProperty "babashka.main") will return the name of the main function.
The environment variable BABASHKA_PRELOADS allows to define code that will be available in all subsequent usages of babashka.
$ bb --classpath script --main my.namespace 1 2 3 Hello from my namespace! 1 2 3
$ tree -L 3
├── deps.edn
├── README
├── src
│ └── project_namespace
│ ├── main.clj
│ └── utilities.clj
└── test
└── project_namespace ├── test_main.clj └── test_utilities.clj
BASH
$ bb --classpath src:test socket-repl 1666 BASH
$ bb -m clojure.core/prn 1 2 3
"1" "2" "3"
CLOJURE
BABASHKA_PRELOADS='(defn foo [x] (+ x 2))'
BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))' export BABASHKA_PRELOADS
BASH
Note that you can concatenate multiple expressions. Now you can use these functions in babashka:
You can also preload an entire file using load-file:
Note that *input* is not available in preloads.
Babashka supports running a REPL, a socket REPL and an nREPL server.
To start a REPL, type:
To get history with up and down arrows, use rlwrap(https://github.com/hanslub42/rlwrap):
To start a socket REPL on port 1666:
Now you can connect with your favorite socket REPL client:
The --socket-repl option takes options similar to the clojure.server.repl Java property option in Clojure:
Editor plugins and tools known to work with a babashka socket REPL:
• Emacs: inf-clojure(https://github.com/clojure-emacs/inf-clojure): To connect:
$ bb '(-> (foo *input*) bar)' <<< 1 6
BASH
export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")' BASH
$ bb repl SHELL
$ rlwrap bb repl SHELL
$ bb socket-repl 1666
Babashka socket REPL started at localhost:1666
SHELL
$ rlwrap nc 127.0.0.1 1666 Babashka v0.0.14 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.
bb=> (+ 1 2 3) 6
bb=> :repl/quit
$
SHELL
$ bb socket-repl '{:address "0.0.0.0" :accept clojure.core.server/repl :port 1666}' CLOJURE
M-x inf-clojure-connect <RET> localhost <RET> 1666 Before evaluating from a Clojure buffer:
M-x inf-clojure-minor-mode
• Atom: Chlorine(https://github.com/mauricioszabo/atom-chlorine)
• Vim: vim-iced(https://github.com/liquidz/vim-iced)
• IntelliJ IDEA: Cursive(https://cursive-ide.com/)
Note: you will have to use a workaround via tubular(https://github.com/mfikes/tubular). For more info, look here (https://cursive-ide.com/userguide/repl.html#repl-types).
Launching a prepl can be done as follows:
or programmatically:
To start an nREPL server:
or programmatically:
Then connect with your favorite nREPL client:
Editor plugins and tools known to work with the babashka nREPL server:
• Emacs: CIDER(https://docs.cider.mx/cider/platforms/babashka.html)
• lein repl :connect
• VSCode: Calva(http://calva.io/)
• Atom: Chlorine(https://github.com/mauricioszabo/atom-chlorine)
• (Neo)Vim: vim-iced(https://github.com/liquidz/vim-iced), conjure(https://github.com/Olical/conjure), fireplace (https://github.com/tpope/vim-fireplace)
$ bb socket-repl '{:address "0.0.0.0" :accept clojure.core.server/io-prepl :port 1666}' CLOJURE
$ bb -e '(clojure.core.server/io-prepl)' (+ 1 2 3)
{:tag :ret, :val "6", :ns "user", :ms 0, :form "(+ 1 2 3)"}
CLOJURE
$ bb nrepl-server 1667 SHELL
$ bb -e "(babashka.nrepl.server/start-server\!) (deref (promise))"
Started nREPL server at 0.0.0.0:1667
CLOJURE
$ lein repl :connect 1667
Connecting to nREPL at 127.0.0.1:1667 user=> (+ 1 2 3)
6 user=>
CLOJURE
The babashka nREPL server currently does not write an .nrepl-port file at startup. Using the following nrepl task, defined in a bb.edn, you can accomplish the same:
The babashka.nrepl.server API is exposed since version 0.8.157.
To debug the nREPL server from the binary you can run:
This will print all the incoming messages.
To debug the nREPL server from source:
For the socket REPL, pREPL, or nREPL, if a randomized port is needed, 0 can be used anywhere a port argument is accepted.
In one-liners the *input* value may come in handy. It contains the input read from stdin as EDN by default. If you want to read in text, use the -i flag, which binds *input* to a lazy seq of lines of text. If you want to read multiple EDN values, use the -I flag. The -o option prints the result as lines of text. The -O option prints the result as lines of EDN values.
*input* is only available in the user namespace, designed for one-liners. For writing scripts, see Scripts.The following table illustrates the combination of options for commands of the form
echo "{{Input}}" | bb {{Input flags}} {{Output flags}} "*input*"
Input Input flags Output flag *input* Output
{:a 1} {:a 2} {:a 1} {:a 1}
{:tasks {nrepl
{:requires ([babashka.fs :as fs]
[babashka.nrepl.server :as srv]) :task (do (srv/start-server! {:host "localhost"
:port 1339}) (spit ".nrepl-port" "1339") (-> (Runtime/getRuntime)
(.addShutdownHook
(Thread. (fn [] (fs/delete ".nrepl-port"))))) (deref (promise)))}}}
CLOJURE
$ BABASHKA_DEV=true bb nrepl-server 1667 SHELL
$ git clone https://github.com/borkdude/babashka --recurse-submodules
$ cd babashka
$ BABASHKA_DEV=true clojure -A:main --nrepl-server 1667
CLOJURE
Input Input flags Output flag *input* Output hello
bye
-i ("hello" "bye") ("hello" "bye")
hello bye
-i -o ("hello" "bye") hello bye
{:a 1} {:a 2} -I ({:a 1} {:a 2}) ({:a 1} {:a 2})
{:a 1} {:a 2} -I -O ({:a 1} {:a 2}) {:a 1} {:a 2}
When combined with the --stream option, the expression is executed for each value in the input:
When writing scripts instead of one-liners on the command line, it is not recommended to use *input*. Here is how you can rewrite to standard Clojure code.
Reading a single EDN value from stdin:
Reading multiple EDN values from stdin (the -I flag):
Reading text from stdin can be done with (slurp *in*). To get a lazy seq of lines (the -i flag), you can use:
To print to stdout, use println for text and prn for EDN values.
The --uberscript option collects the expressions in BABASHKA_PRELOADS, the command line expression or file, the
$ echo '{:a 1} {:a 2}' | bb --stream '*input*' {:a 1}
{:a 2}
CLOJURE
(ns script
(:require [clojure.edn :as edn])) (edn/read *in*)
CLOJURE
(ns script
(:require [clojure.edn :as edn]
[clojure.java.io :as io]))
(let [reader (java.io.PushbackReader. (io/reader *in*))]
(take-while #(not (identical? ::eof %)) (repeatedly #(edn/read {:eof ::eof} reader))))
CLOJURE
(ns script
(:require [clojure.java.io :as io])) (line-seq (io/reader *in*))
CLOJURE
main entrypoint and all required namespaces from the classpath into a single file. This can be convenient for debugging and deployment.
Here is an example that uses a function from the clj-commons/fs(https://github.com/clj-commons/fs) library.
Let’s first set the classpath:
Write a little script, say glob.clj:
For testing, we’ll make a file which we will find using the glob function:
Now we can execute the script which uses the library:
Producing an uberscript with all required code:
To prove that we don’t need the classpath anymore:
Caveats:
• Dynamic requires. Building uberscripts works by running top-level ns and require forms. The rest of the code is not evaluated. Code that relies on dynamic requires may not work in an uberscript.
• Resources. The usage of io/resource assumes a classpath, so when this is used in your uberscript, you still have to set a classpath and bring the resources along.
If any of the above is problematic for your project, using an uberjar is a good alternative.
Uberscripts can be optimized by cutting out unused vars with carve(https://github.com/borkdude/carve).
$ export BABASHKA_CLASSPATH=$(clojure -Spath -Sdeps '{:deps {clj-commons/fs {:mvn/version "1.6.307"}}}') CLOJURE
(ns glob (:require [me.raynes.fs :as fs]))
(run! (comp println str)
(fs/glob (first *command-line-args*)))
CLOJURE
$ touch README.md BASH
$ time bb glob.clj '*.md' /private/tmp/glob/README.md
bb glob.clj '*.md' 0.03s user 0.01s system 88% cpu 0.047 total
BASH
$ bb uberscript glob-uberscript.clj -f glob.clj BASH
$ unset BABASHKA_CLASSPATH
$ time bb glob-uberscript.clj '*.md' /private/tmp/glob/README.md
bb glob-uberscript.clj '*.md' 0.03s user 0.02s system 93% cpu 0.049 total
BASH
BASH
Note that the uberscript became 72% shorter. This has a beneficial effect on execution time:
Babashka can create uberjars from a given classpath and optionally a main method:
Babashka sets the following system properties:
• babashka.version: the version string, e.g. "1.2.0"
• babashka.main: the --main argument
• babashka.file: the --file argument (normalized using .getAbsolutePath)
Data readers can be enabled by setting *data-readers* to a hashmap of symbols to functions or vars:
To preserve good startup time, babashka does not scan the classpath for data_readers.clj files.
Babashka supports reader conditionals by taking either the :bb or :clj branch, whichever comes first. NOTE: the :clj branch behavior was added in version 0.0.71, before that version the :clj branch was ignored.
$ wc -l glob-uberscript.clj 583 glob-uberscript.clj
$ carve --opts '{:paths ["glob-uberscript.clj"] :aggressive true :silent true}'
$ wc -l glob-uberscript.clj 105 glob-uberscript.clj
$ time bb glob-uberscript.clj '*.md' /private/tmp/glob/README.md
bb glob-uberscript.clj '*.md' 0.02s user 0.01s system 84% cpu 0.034 total
BASH
$ cat bb/foo.clj (ns foo)
(defn -main [& args] (prn :hello))
$ cat bb.edn {:paths ["bb"]}
$ bb uberjar foo.jar -m foo
$ bb foo.jar :hello
CLOJURE
$ bb -e "(set! *data-readers* {'t/tag inc}) #t/tag 1"
2
CLOJURE
$ bb -e "#?(:bb :hello :clj :bye)"
:hello
$ bb -e "#?(:clj :bye :bb :hello)"
:bye
$ bb -e "[1 2 #?@(:bb [] :clj [1])]"
[1 2]
CLOJURE
Babashka bundles deps.clj(https://github.com/borkdude/deps.clj) for invoking a clojure JVM process:
See the clojure function in the babashka.deps namespace for programmatically invoking clojure.
$ bb clojure -M -e "*clojure-version*"
{:major 1, :minor 10, :incremental 1, :qualifier nil}
CLOJURE
Since version 0.3.1, babashka supports a local bb.edn file to manage a project.
You can declare one or multiple paths and dependencies so they are automatically added to the classpath:
If we have a project that has a deps.edn and would like to reuse those deps in bb.edn:
Use a unique name to refer to your project’s deps.edn, the same name that you would otherwise use when referring to your project as a dependency.If we have a main function in a file called bb/my_project/main.clj like:
we can invoke it like:
See Invoking a main function for more details on how to invoke a function from the command line.
The :deps entry is managed by deps.clj(https://github.com/borkdude/deps.clj) and requires a java installation to resolve and download dependencies.
Since version 0.3.6, babashka supports the :min-bb-version where the minimal babashka version can be declared:
When using an older bb version (that supports :min-bb-version), babashka will print a warning:
{:paths ["bb"]
:deps {medley/medley {:mvn/version "1.3.0"}}}
CLOJURE
{:deps {your-org/your-project {:local/root "."}}} CLOJURE
(ns my-project.main
(:require [medley.core :as m]))
(defn -main [& _args]
(prn (m/index-by :id [{:id 1} {:id 2}])))
$ bb -m my-project.main {1 {:id 1}, 2 {:id 2}}
CLOJURE
{:paths ["src"]
:deps {medley/medley {:mvn/version "1.3.0"}}
:min-bb-version "0.3.7"}
CLOJURE
WARNING: this project requires babashka 0.3.7 or newer, but you have: 0.3.6
Since babashka 0.4.0 the bb.edn file supports the :tasks entry which describes tasks that you can run in the current project. The tasks feature is similar to what people use Makefile, Justfile or npm run for. See Task runner for more details.
Since babashka 1.3.177 a bb.edn file relative to the invoked file is respected. This makes writing system-global scripts with dependencies easier.
Given a bb.edn:
and a script medley.bb:
Assuming that medley.bb is executable (chmod +x medley.bb), you can directly execute it in the current directory:
To execute this script from anywhere on the system, you just have to add it to the PATH:
Of course you can just call your script medley without the .bb extension.
On Windows bash shebangs are not supported. An alternative is to create a script-adjacent .bat file, e.g medley.bat:
Then add this script to your %PATH%:
{:deps {medley/medley {:mvn/version "1.3.0"}}} CLOJURE
#!/usr/bin/env bb
(ns medley
(:require [medley.core :as medley])) (prn (medley/index-by :id [{:id 1}]))
CLOJURE
~/my_project $ ./medley.bb {1 {:id 1}}
SHELL
/tmp $ export PATH=$PATH:~/my_project # ensure script is on path /tmp $ medley.bb # works, respects ~/my_project/bb.edn file with :deps {1 {:id 1}}
SHELL
@echo off set ARGS=%*
set SCRIPT=%~dp0medley.bb bb %SCRIPT% %ARGS%
SHELL
C:\Temp> set PATH=%PATH%;c:\my_project C:\Temp> medley
{1 {:id 1}}
SHELL
People often use a Makefile, Justfile, npm scripts or lein aliases in their (clojure) projects to remember complex invocations and to create shortcuts for them. Since version 0.4.0, babashka supports a similar feature as part of the bb.edn project configuration file. For a general overview of what’s available in bb.edn, go to Project setup.
The tasks configuration lives under the :tasks key and can be used together with :paths and :deps:
In the above example we see a simple task called clean which invokes the shell command, to remove the target directory. You can invoke this task from the command line with:
Babashka also accepts a task name without explicitly mentioning run:
To make your tasks more cross-platform friendly, you can use the built-in babashka.fs(https://github.com/babashka/fs) library. To use libraries in tasks, use the :requires option:
Tasks accept arbitrary Clojure expressions. E.g. you can print something when executing the task:
Go here(https://www.youtube.com/watch?v=u5ECoR7KT1Y&ab_channel=LondonClojurians) if you would like to watch a talk on babashka tasks.
{:paths ["script"]
:deps {medley/medley {:mvn/version "1.3.0"}}
:min-bb-version "0.4.0"
:tasks
{clean (shell "rm -rf target") ...}
}
CLOJURE
$ bb run clean BASH
$ bb clean BASH
{:tasks
{:requires ([babashka.fs :as fs]) clean (fs/delete-tree "target") }
}
CLOJURE
{:tasks
{:requires ([babashka.fs :as fs])
clean (do (println "Removing target folder.") (fs/delete-tree "target"))
} }
CLOJURE
$ bb clean
Removing target folder.
BASH
Instead of naked expressions, tasks can be defined as maps with options. The task expression should then be moved to the :task key:
Tasks support the :doc option which gives it a docstring which is printed when invoking bb tasks on the command line. Other options include:
• :requires: task-specific namespace requires.
• :extra-paths: add paths to the classpath.
• :extra-deps: add extra dependencies to the classpath.
• :enter, :leave: override the global :enter/:leave hook.
When invoking bb tasks, babashka prints a list of all tasks found in bb.edn in the order of appearance. E.g. in the clj- kondo.lsp(https://github.com/clj-kondo/clj-kondo.lsp) project it prints:
Command line arguments are available as *command-line-args*, just like in Clojure. Since version 0.9.160, you can use babashka.cli(https://github.com/babashka/cli) in tasks via the exec function to deal with command line arguments in a concise way. See the chapter on babashka CLI.
Of course, you are free to parse command line arguments using the built-in tools.cli library or just handle them manually.
You can re-bind *command-line-args* to ensure functions see a different set of arguments:
{:tasks {
clean {:doc "Removes target folder"
:requires ([babashka.fs :as fs]) :task (fs/delete-tree "target")}
} }
CLOJURE
$ bb tasks
The following tasks are available:
recent-clj-kondo Detects most recent clj-kondo version from clojars update-project-clj Updates project.clj with most recent clj-kondo version java1.8 Asserts that we are using java 1.8
build-server Produces lsp server standalone jar
lsp-jar Copies renamed jar for upload to clj-kondo repo upload-jar Uploads standalone lsp server jar to clj-kondo repo vscode-server Copied lsp server jar to vscode extension
vscode-version Prepares package.json with up to date clj-kondo version vscode-publish Publishes vscode extension to marketplace
ovsx-publish Publishes vscode extension to ovsx thing
publish The mother of all tasks: publishes everything needed for new release
BASH
CLOJURE
Add this to your .zshrc to get tab-complete feature on ZSH.
Add this to your .bashrc to get tab-complete feature on bash.
Add this to your .config/fish/completions/bb.fish to get tab-complete feature on Fish shell.
You can execute tasks using bb <task-name>. The babashka run subcommand can be used to execute with some additinoal options:
• --parallel: invoke task dependencies in parallel.
{:tasks
{:init (do (defn print-args []
(prn (:name (current-task))
*command-line-args*))) bar (print-args)
foo (do (print-args)
(binding [*command-line-args* (next *command-line-args*)]
(run 'bar)))}}
$ bb foo 1 2 3 foo ("1" "2" "3") bar ("2" "3")
BASH
_bb_tasks() {
local matches=(`bb tasks |tail -n +3 |cut -f1 -d ' '`) compadd -a matches
_files # autocomplete filenames as well }
compdef _bb_tasks bb
BASH
_bb_tasks() {
COMPREPLY=( $(compgen -W "$(bb tasks |tail -n +3 |cut -f1 -d ' ')" -- ${COMP_WORDS[COMP_CWORD]}) );
}
# autocomplete filenames as well complete -f -F _bb_tasks bb
BASH
function __bb_complete_tasks if not test "$__bb_tasks"
set -g __bb_tasks (bb tasks |tail -n +3 |cut -f1 -d ' ') end
printf "%s\n" $__bb_tasks end
complete -c bb -a "(__bb_complete_tasks)" -d 'tasks'
BASH
CLOJURE
Also see Parallel tasks.
• --prn: print the result from the task expression:
Unlike scripts, babashka tasks do not print their return value.
The task runner exposes the following hooks:
The :init is for expressions that are executed before any of the tasks are executed. It is typically used for defining helper functions and constants:
The :enter hook is executed before each task. This is typically used to print the name of a task, which can be obtained using the current-task function:
{:tasks
{:init (def log (Object.)) :enter (locking log
(println (str (:name (current-task))
":")
(java.util.Date.))) a (Thread/sleep 5000)
b (Thread/sleep 5000) c {:depends [a b]}
d {:task (time (run 'c))}}}
$ bb run --parallel d
d: #inst "2021-05-08T14:14:56.322-00:00"
a: #inst "2021-05-08T14:14:56.357-00:00"
b: #inst "2021-05-08T14:14:56.360-00:00"
c: #inst "2021-05-08T14:15:01.366-00:00"
"Elapsed time: 5023.894512 msecs"
BASH
{:tasks {sum (+ 1 2 3)}} CLOJURE
$ bb run --prn sum 6
BASH
{:tasks
{:init (defn env [s] (System/getenv s))
print-env (println (env (first *command-line-args*))) }
}
CLOJURE
$ FOO=1 bb print-env FOO 1
BASH
CLOJURE
The :leave hook is similar to :enter but is executed after each task.
Both hooks can be overriden as task-local options. Setting them to nil will disable them for specific tasks (see Task- local options).
The babashka.tasks namespace exposes the following functions: run, shell, clojure and current-task. They are implicitly imported, thus available without a namespace prefix.
Tasks provide the run function to explicitly invoke another task:
When running bb uberjar:clean, first the clean task is executed and the uberjar:
The clojure function in the above example executes a clojure process using deps.clj(https://github.com/borkdude/deps.clj). See clojure for more info
The run function accepts an additional map with options:
The :parallel option executes dependencies of the invoked task in parallel (when possible). See Parallel tasks.
Both shell and clojure return a process(https://github.com/babashka/babashka.process) object which returns the :exit code among other info. By default these functions will throw an exception when a non-zero exit code was returned and
{:tasks
{:init (defn env [s] (System/getenv s))
:enter (println "Entering:" (:name (current-task))) print-env (println (env (first *command-line-args*))) }
}
$ FOO=1 bb print-env FOO Entering: print-env 1
BASH
{:tasks
{:requires ([babashka.fs :as fs])
clean (do
(println "Removing target folder.") (fs/delete-tree "target"))
uberjar (do
(println "Making uberjar") (clojure "-X:uberjar")) uberjar:clean (do (run 'clean)
(run 'uberjar))}
}
CLOJURE
$ bb uberjar:clean Removing target folder.
Making uberjar
BASH
they will inherit the stdin/stdout/stderr from the babashka process.
You can opt out of this behavior by using the :continue option:
When you want to redirect output to a file instead, you can provide the :out option.
To run a program in another directory, you can use the :dir option:
To set environment variables with shell or clojure:
Other supported options are similar to those of babashka.process/process (https://github.com/babashka/babashka.process).
The process is executed synchronously: i.e. babashka will wait for the process to finish before executing the next expression. If this doesn’t fit your use case, you can use babashka.process/process
(https://github.com/babashka/babashka.process) directly instead. These two invocations are roughly equivalent:
{:tasks {
ls (shell "ls foo") }
}
CLOJURE
$ bb ls
ls: foo: No such file or directory Error while executing task: ls
$ echo $?
1
BASH
{:tasks {
ls (shell {:continue true} "ls foo") }
}
CLOJURE
$ bb ls
ls: foo: No such file or directory
$ echo $?
0
BASH
(shell {:out "file.txt"} "echo hello") CLOJURE
(shell {:dir "subproject"} "ls") CLOJURE
(shell {:extra-env {"FOO" "BAR"}} "printenv FOO") CLOJURE
Note that the first string argument to shell it tokenized (broken into multiple parts) and the trailing arguments are not:
Correct:
Not correct (-g nbb within the same string):
Note that the varargs signature plays well with feeding *command-line-args*:
Note that shell does not invoke a shell but just shells out to an external program. As such, shell does not
understand bash specific syntax. The following does not work: (shell "rm -rf target/*"). To invoke a specific shell, you should do that explicitly with:
Also see the docstring of shell here(https://github.com/babashka/process/blob/master/API.md#shell).
The clojure function starts a Clojure process using deps.clj(https://github.com/borkdude/deps.clj). The interface is exactly the same as the clojure CLI. E.g. to evaluate an expression:
or to invoke clj-kondo as a library:
The clojure task function behaves similar to shell with respect to the exit code, return value and supported
options, except when it comes to features that do not start a process, but only do some printing. E.g. you can capture the classpath using:
because this operation doesn’t start a process but prints to *out*.
(require '[babashka.process :as p :refer [process]]
'[babashka.tasks :as tasks])
(tasks/shell {:dir "subproject"} "npm install")
(-> (process {:dir "subproject" :inherit true} "npm install") (p/check))
CLOJURE
(shell "npm install" "-g" "nbb") CLOJURE
(shell "npm install" "-g nbb") CLOJURE
(apply shell "npm install" *command-line-args*) CLOJURE
(shell "bash -c" "rm -rf target/*") CLOJURE
{:tasks {eval (clojure "-M -e '(+ 1 2 3)'")}} CLOJURE
{:tasks {eval (clojure {:dir "subproject"} "-M:clj-kondo")}} CLOJURE
(with-out-str (clojure "-Spath")) CLOJURE
To run a clojure task in another directory:
The current-task function returns a map representing the currently running task. This function is typically used in the :enter and :leave hooks.
See exec.
Dependencies between tasks can be declared using :depends:
The fs/modified-since function returns a seq of all newer files compared to a target, which can be used to prevent rebuilding artifacts when not necessary.
Alternatively you can use the :init hook to define vars, require namespaces, etc.:
It is common to define tasks that only serve as a helper to other tasks. To not expose these tasks in the output of bb tasks, you can start their name with a hyphen.
The :parallel option executes dependencies of the invoked task in parallel (when possible). This can be used to speed up execution, but also to have multiple tasks running in parallel for development:
{:tasks {eval (clojure {:dir "subproject"} "-M:clj-kondo")}} CLOJURE
{:tasks {:requires ([babashka.fs :as fs]) -target-dir "target"
-target {:depends [-target-dir]
:task (fs/create-dirs -target-dir)}
-jar-file {:depends [-target]
:task "target/foo.jar"}
jar {:depends [-target -jar-file]
:task (when (seq (fs/modified-since -jar-file
(fs/glob "src" "**.clj"))) (spit -jar-file "test")
(println "made jar!"))}
uberjar {:depends [jar]
:task (println "creating uberjar!")}}}
CLOJURE
{:tasks {:requires ([babashka.fs :as fs]) :init (do (def target-dir "target")
(def jar-file "target/foo.jar")) -target {:task (fs/create-dirs target-dir)}
jar {:depends [-target]
:task (when (seq (fs/modified-since jar-file
(fs/glob "src" "**.clj"))) (spit jar-file "test")
(println "made jar!"))}
uberjar {:depends [jar]
:task (println "creating uberjar!")}}}
CLOJURE
CLOJURE
The dev task invokes the (private) -dev task in parallel
The -dev task depends on three other tasks which are executed simultaneously.
Invoking a main function can be done by providing a fully qualified symbol:
You can use any fully qualified symbol, not just ones that end in -main (so e.g. foo.bar/baz is fine). You can also have multiple main functions in one namespace.
The namespace foo.bar will be automatically required and the function will be invoked with *command-line- args*:
To get a REPL within a task, you can use clojure.main/repl:
Alternatively, you can use babashka.tasks/run to invoke a task from a REPL.
For REPL- and linting-friendliness, it’s recommended to move task code longer than a couple of lines to a .clj or .bb file.
• antq(https://github.com/borkdude/antq/blob/bb-run/bb.edn)
• mach(https://github.com/borkdude/mach/blob/bb-run/examples/app/bb.edn)
• bb.edn at Doctor Evidence(https://gist.github.com/borkdude/35bc0a20bd4c112dec2c5645f67250e3)
• clj-kondo.lsp(https://github.com/clj-kondo/clj-kondo.lsp/blob/master/bb.edn)
• pathom(https://github.com/wilkerlucio/pathom-viz/blob/master/bb.edn)
• rssyslib(https://github.com/redstarssystems/rssyslib/blob/develop/bb.edn)
• rewrite-clj(https://github.com/clj-commons/rewrite-clj/blob/main/bb.edn)
• https://gist.github.com/delyada/9f50fa7466358e55f27e4e6b4314242f
dev {:doc "Runs app in dev mode. Compiles cljs, less and runs JVM app in parallel."
:task (run '-dev {:parallel true})}
-dev {:depends [dev:cljs dev:less dev:backend]}
dev:cljs {:doc "Runs front-end compilation"
:task (clojure "-M:frontend:cljs/dev")}
dev:less {:doc "Compiles less"
:task (clojure "-M:frontend:less/dev")}
dev:backend {:doc "Runs backend in dev mode"
:task (clojure (str "-A:backend:backend/dev:" platform-alias)
"-X" "dre.standalone/start")}
{:tasks
{foo-bar foo.bar/-main}}
CLOJURE
$ bb foo-bar 1 2 3 CLOJURE
{:tasks {repl (clojure.main/repl)}} CLOJURE
• jirazzz(https://github.com/rwstauner/jirazzz/blob/main/bb.edn)
When running a task, babashka assembles a small program which defines vars bound to the return values of tasks.
This brings the limitation that you can only choose names for your tasks that are valid as var names. You can’t name your task foo/bar for this reason. If you want to use delimiters to indicate some sort of grouping, you can do it like
foo-bar, foo:bar or foo_bar.
Names starting with a - are considered "private" and not listed in the bb tasks output.
bb <option> is resolved in the order of file > task > subcommand.
Escape hatches in case of conflicts:
• execute relative file as bb ./foo
• execute task as bb run foo
• execute subcommand as bb --foo
You can name a task similar to a core var, let’s say: format. If you want to refer to the core var, it is recommended to use the fully qualified clojure.core/format in that case, to avoid conflicts in :enter and :leave expressions and when using the format task as a dependency.
Because bb.edn is an EDN file, you cannot use all of Clojure’s syntax in expressions. Most notably:
• You cannot use #(foo %), but you can use (fn [x] (foo x))
• You cannot use @(foo) but you can use (deref foo)
• You cannot use #"re" but you can use (re-pattern "re")
• Single quotes are accidentally supported in some places, but are better avoided: {:task '(foo)} does not work, but {:task (quote (foo)) does work. When requiring namespaces, use the :requires feature in favor of doing it manually using (require '[foo]).
In version 0.9.160 of babashka, the babashka CLI(https://github.com/babashka/cli) added as a built-in library together with task integration.
For invoking functions from the command line, you can use the new -x flag (a pun to Clojure’s -X of course!):
What we see in the above snippet is that a map {:hello "there"} is constructed by babashka CLI and then fed to the prn function. After that the result is printed to the console.
What if we want to influence how things are parsed by babashka CLI and provide some defaults? This can be done using metadata. Let’s create a bb.edn and make a file available on the classpath:
bb.edn:
tasks.clj:
Now let’s invoke:
As you can see, the namespace options are merged with the function options. Defaults can be provided with :exec- args, like you’re used to from the clojure CLI.
What about task integration? Let’s adapt our bb.edn:
and invoke the task:
bb -x clojure.core/prn --hello there {:hello "there"}
CLOJURE
{:paths ["."]} CLOJURE
(ns tasks
{:org.babashka/cli {:exec-args {:ns-data 1}}})
(defn my-function
{:org.babashka/cli {:exec-args {:fn-data 1}
:coerce {:num [:int]}
:alias {:n :num}}}
[m]
(prn m))
CLOJURE
$ bb -x tasks/my-function -n 1 2 {:ns-data 1, :fn-data 1, :num [1 2]}
CLOJURE
{:paths ["."]
:tasks {doit {:task (let [x (exec 'tasks/my-function)]
(prn :x x))
:exec-args {:task-data 1234}}
}}
CLOJURE
CLOJURE
As you can see it works similar to -x, but you can provide another set of defaults on the task level with :exec-args. Executing a function through babashka CLI is done using the babashka.task/exec function, available by default in tasks.
To add :exec-args that should be evaluated you can pass an extra map to exec as follows:
$ bb doit --cli-option :yeah -n 1 2 3
:x {:ns-data 1, :fn-data 1, :task-data 1234, :cli-option :yeah, :num [1 2 3]}
{:paths ["."]
:tasks {doit {:task (let [x (exec 'tasks/my-function {:exec-args {:foo (+ 1 2 3)}})]
(prn :x x))
:exec-args {:task-data 1234}}
}}
CLOJURE
$ bb doit --cli-option :yeah -n 1 2 3
:x {:ns-data 1, :fn-data 1, :task-data 1234, :cli-option :yeah, :num [1 2 3] :foo 6}
CLOJURE
In addition to clojure.core, the following libraries / namespaces are available in babashka. Some are available through pre-defined aliases in the user namespace, which can be handy for one-liners. If not all vars are available, they are enumerated explicitly. If some important var is missing, an issue or PR is welcome.
From Clojure:
• clojure.core
• clojure.core.protocols: Datafiable, Navigable
• clojure.data
• clojure.datafy
• clojure.edn aliased as edn
• clojure.math
• clojure.java.browse
• clojure.java.io aliased as io:
◦ as-relative-path, as-url, copy, delete-file, file, input-stream, make-parents, output-stream, reader, resource, writer
• clojure.java.shell aliased as shell
• clojure.main: demunge, repl, repl-requires
• clojure.pprint: pprint, cl-format
• clojure.set aliased as set
• clojure.string aliased as str
• clojure.stacktrace
• clojure.test
• clojure.zip
Additional libraries:
• babashka.cli (https://github.com/babashka/cli): CLI arg parsing
• babashka.http-client (https://github.com/babashka/http-client): making HTTP requests
• babashka.process (https://github.com/babashka/process): shelling out to external processes
• babashka.fs (https://github.com/babashka/fs): file system manipulation
• bencode.core (https://github.com/nrepl/bencode) aliased as bencode: read-bencode, write-bencode
• cheshire.core (https://github.com/dakrone/cheshire) aliased as json: dealing with JSON
• clojure.core.async (https://clojure.github.io/core.async/) aliased as async.
• clojure.data.csv (https://github.com/clojure/data.csv) aliased as csv
• clojure.data.xml (https://github.com/clojure/data.xml) aliased as xml
• clojure.tools.cli (https://github.com/clojure/tools.cli) aliased as tools.cli
• clj-yaml.core (https://github.com/clj-commons/clj-yaml) alias as yaml
• cognitect.transit (https://github.com/cognitect/transit-clj) aliased as transit
• org.httpkit.client (https://github.com/http-kit/http-kit)
• org.httpkit.server (https://github.com/http-kit/http-kit)
• clojure.core.match (https://github.com/clojure/core.match)
• hiccup.core (https://github.com/weavejester/hiccup/) and hiccup2.core
• clojure.test.check (https://github.com/clojure/test.check):
◦ clojure.test.check
◦ clojure.test.check.generators
◦ clojure.test.check.properties
• rewrite-clj (https://github.com/clj-commons/rewrite-clj):
◦ rewrite-clj.parser
◦ rewrite-clj.node
◦ rewrite-clj.zip
◦ rewrite-clj.paredit
• Selmer (https://github.com/yogthos/Selmer):
◦ selmer.parser
• clojure.tools.logging (https://github.com/clojure/tools.logging)
• timbre (https://github.com/ptaoussanis/timbre): logging
• edamame (https://github.com/borkdude/edamame): Clojure parser
• core.rrb-vector (https://github.com/clojure/core.rrb-vector)
Check out the babashka toolbox(https://babashka.org/toolbox/) and projects
(https://github.com/borkdude/babashka/blob/master/doc/projects.md) page for libraries that are not built-in, but which you can load as an external dependency in bb.edn (https://book.babashka.org/#_bb_edn).
See the build(https://github.com/borkdude/babashka/blob/master/doc/build.md) page for built-in libraries that can be enabled via feature flags, if you want to compile babashka yourself.
A selection of Java classes are available, see babashka/impl/classes.clj
(https://github.com/babashka/babashka/blob/master/src/babashka/impl/classes.clj) in babashka’s git repo.
Available functions:
• add-classpath
• get-classpath
• split-classpath
The function add-classpath which can be used to add to the classpath dynamically:
The function get-classpath returns the classpath as set by --classpath, BABASHKA_CLASSPATH and add- classpath.
Given a classpath, returns a seq of strings as the result of splitting the classpath by the platform specific path separatator.
Available functions:
• add-deps
• clojure
• merge-deps
The function add-deps takes a deps edn map like {:deps {medley/medley {:mvn/version "1.3.0"}}}, resolves it using deps.clj(https://github.com/borkdude/deps.clj) and then adds to the babashka classpath accordingly.
Example:
Optionally, add-deps takes a second arg with options. Currently the only option is :aliases which will affect how deps are resolved:
Example:
The function clojure takes a sequential collection of arguments, similar to the clojure CLI. The arguments are then passed to deps.clj(https://github.com/borkdude/deps.clj). The clojure function returns nil and prints to *out* for
(require '[babashka.classpath :refer [add-classpath]]
'[clojure.java.shell :refer [sh]]
'[clojure.string :as str])
(def medley-dep '{:deps {medley {:git/url "https://github.com/borkdude/medley"
:sha "91adfb5da33f8d23f75f0894da1defe567a625c0"}}}) (def cp (-> (sh "clojure" "-Spath" "-Sdeps" (str medley-dep)) :out str/trim)) (add-classpath cp)
(require '[medley.core :as m])
(m/index-by :id [{:id 1} {:id 2}]) ;;=> {1 {:id 1}, 2 {:id 2}}
CLOJURE
(require '[babashka.deps :as deps])
(deps/add-deps '{:deps {medley/medley {:mvn/version "1.3.0"}}})
(require '[medley.core :as m]) (m/index-by :id [{:id 1} {:id 2}])
CLOJURE
(deps/add-deps '{:aliases {:medley {:extra-deps {medley/medley {:mvn/version "1.3.0"}}}}}
{:aliases [:medley]})
CLOJURE
commands like -Stree, and -Spath. For -M, -X and -A it invokes java with babashka.process/process (see babashka.process) and returns the associated record. For more details, read the docstring with:
Example:
The following script passes through command line arguments to clojure, while adding the medley dependency:
Contains the functions: wait-for-port and wait-for-path. Usage of wait-for-port:
Waits for TCP connection to be available on host and port. Options map supports :timeout and :pause. If :timeout is provided and reached, :default's value (if any) is returned. The :pause option determines the time waited between retries.
Usage of wait-for-path:
Waits for file path to be available. Options map supports :default, :timeout and :pause. If :timeout is provided and reached, :default's value (if any) is returned. The :pause option determines the time waited between retries.
The namespace babashka.wait is aliased as wait in the user namespace.
Contains the function signal/pipe-signal-received?. Usage:
Returns true if PIPE signal was received. Example:
(require '[clojure.repl :refer [doc]]) (doc babashka.deps/clojure)
CLOJURE
(require '[babashka.deps :as deps])
(def deps '{:deps {medley/medley {:mvn/version "1.3.0"}}}) (def clojure-args (list* "-Sdeps" deps *command-line-args*))
(if-let [proc (deps/clojure clojure-args)]
(-> @proc :exit (System/exit)) (System/exit 0))
CLOJURE
(wait/wait-for-port "localhost" 8080)
(wait/wait-for-port "localhost" 8080 {:timeout 1000 :pause 1000})
CLOJURE
(wait/wait-for-path "/tmp/wait-path-test")
(wait/wait-for-path "/tmp/wait-path-test" {:timeout 1000 :pause 1000})
CLOJURE
(signal/pipe-signal-received?) CLOJURE
$ bb -e '((fn [x] (println x) (when (not (signal/pipe-signal-received?)) (recur (inc x)))) 0)' | head -n2 1
2
BASH
The namespace babashka.signal is aliased as signal in the user namespace.
The babashka.http-client library for making HTTP requests. See babashka.http-client (https://github.com/babashka/http-client) for how to use it.
The babashka.process library. See the process(https://github.com/babashka/process) repo for API docs.
The babashka.fs library offers file system utilities. See the fs(https://github.com/babashka/fs) repo for API docs.
The babashka.cli library allows you to turn functions into CLIs. See the cli(https://github.com/babashka/cli) repo for API docs and check out the babashka CLI(https://book.babashka.org/#_babashka_cli) chapter on how to use it from the command line or with tasks(https://book.babashka.org/#tasks).
Babashka is able to run Clojure projects from source, if they are compatible with the subset of Clojure that sci is capable of running.
Check this page(https://github.com/borkdude/babashka/blob/master/doc/projects.md) for projects that are known to work with babashka.
Do you have a library that is compatible with babashka? Add the official badge to give some flair to your repo!
babashka compatible (https://babashka.org)