A sprinkle of Clojure for the command line.
$ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install)
$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *in*)'
("doc" "resources" "sci" "script" "src" "target" "test")
bb took 4ms.
If you're a bash expert, you probably don't need this. But for those of us who can use a bit of Clojure in their shell scripts, it may be useful.
Babashka runs as a GraalVM binary which results in faster startup time than Clojure on the JVM.
The sweet spot for babashka is executing short Clojure snippets in the same space where you would use bash.
Where it can, babashka calls the regular implementation of Clojure on the JVM
and proxies common Java packages like System
and File
, so writing code in it
should be familiar if you're already using Clojure on the JVM.
Reasons why babashka may not be the right fit for your use case:
load-file
for
loading external scripts).Experimental. Breaking changes are expected to happen at this phase.
$ ls | bb -i '*in*'
["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "reflection.json" "resources" "script" "src" "target" "test"]
$ ls | bb -i '(count *in*)'
12
$ bb '(vec (dedupe *in*))' <<< '[1 1 1 1 2]'
[1 2]
$ bb '(filterv :foo *in*)' <<< '[{:foo 1} {:bar 2}]'
[{:foo 1}]
$ bb '(#(+ %1 %2 %3) 1 2 *in*)' <<< 3
6
$ ls | bb -i '(filterv #(re-find #"reflection" %) *in*)'
["reflection.json"]
$ bb '(run! #(shell/sh "touch" (str "/tmp/test/" %)) (range 100))'
$ ls /tmp/test | bb -i '*in*'
["0" "1" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "2" "20" "21" ...]
$ bb -O '(repeat "dude")' | bb --stream '(str *in* "rino")' | bb -I '(take 3 *in*)'
("duderino" "duderino" "duderino")
More examples can be found in the gallery.
Linux and macOS binaries are provided via brew.
Install:
brew install borkdude/brew/babashka
Upgrade:
brew upgrade babashka
Install via the installer script:
$ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install)
By default this will install into /usr/local/bin
. To change this, provide the directory name:
$ bash <(curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install) /tmp
You may also download a binary from Github.
Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] ( -e <expression> | -f <file> | --socket-repl [<host>:]<port> )
Options:
--help, -h or -?: print this help text.
--version: print the current version of babashka.
-i: bind *in* to a lazy seq of lines from stdin.
-I: bind *in* to a lazy seq of EDN values from stdin.
-o: write lines to stdout.
-O: write EDN values to stdout.
--stream: stream over lines or EDN values from stdin. Combined with -i or -I *in* becomes a single value per iteration.
-e, --eval <expression>: evaluate an expression
-f, --file <path>: evaluate a file
--socket-repl: start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
--time: print execution time before exiting.
If neither -e, -f, or --socket-repl are specified, then the first argument that is not parsed as a option is treated as a file if it exists, or as an expression otherwise.
Everything after that is bound to *command-line-args*.
The clojure.core
functions are accessible without a namespace alias.
The following namespaces are required by default and available through the
pre-defined aliases. You may use require
+ :as
and/or :refer
on these
namespaces. If not all vars are available, they are enumerated explicitly.
clojure.string
aliased as str
clojure.set
aliased as set
clojure.edn
aliased as edn
:
read-string
clojure.java.shell
aliases as shell
:
sh
clojure.java.io
aliased as io
:
as-relative-path
, copy
, delete-file
, file
clojure.core.async
aliased as
async
. The alt
and go
macros are not available but alts!!
does work as
it is a function.me.raynes.conch.low-level
aliased as conch
clojure.tools.cli
aliased as tools.cli
From Java the following is available:
Integer
: Integer/parseInt
File
: .canRead
, .canWrite
, .delete
, .deleteOnExit
, .exists
,
.getAbsoluteFile
, .getCanonicalFile
, .getCanonicalPath
, .getName
,
.getParent
, .getParentFile
, .getPath
, .isAbsolute
, .isDirectory
,
.isFile
, .isHidden
, .lastModified
, .length
, .list
, .listFiles
,
.mkdir
, .mkdirs
, .renameTo
, .setLastModified
, .setReadOnly
,
.setReadable
, .toPath
, .toURI
.System
: exit
, getProperty
, setProperty
, getProperties
, getenv
Thread
: sleep
Special vars:
*in*
: contains the input read from stdin. EDN by default, multiple lines of
text with the -i
option, or multiple EDN values with the -I
option.*command-line-args*
: contain the command line argsAdditionally, babashka adds the following functions:
net/wait-for-it
. Usage:(net/wait-for-it "localhost" 8080)
(net/wait-for-it "localhost" 8080 {:timeout 1000 :pause 1000)
Waits for TCP connection to be available on host and port. Options map supports
:timeout
and :pause
. If :timeout
is provided and reached, exception will
be thrown. The :pause
option determines the time waited between retries.
sig/pipe-signal-received?
. Usage:(sig/pipe-signal-received?)
Returns true if PIPE
signal was received. Example:
$ bb '((fn [x] (println x) (when (not (sig/pipe-signal-received?)) (recur (inc x)))) 0)' | head -n2
1
2
Scripts may be executed from a file using -f
or --file
:
bb -f download_html.clj
Files can also be loaded inline using load-file
:
bb '(load-file "script.clj")'
Using bb
with a shebang also works:
#!/usr/bin/env bb
(defn get-url [url]
(println "Fetching url:" url)
(let [{:keys [:exit :err :out]} (shell/sh "curl" "-sS" url)]
(if (zero? exit) out
(do (println "ERROR:" err)
(System/exit 1)))))
(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 (get-url url)))
(System/exit 0)
$ ./download_html.clj
Usage: <url> <file>
$ ./download_html.clj https://www.clojure.org /tmp/clojure.org.html
Fetching url: https://www.clojure.org
Writing file: /tmp/clojure.org.html
If /usr/bin/env
doesn't work for you, you can use the following workaround:
$ cat script.clj
#!/bin/sh
#_(
"exec" "bb" "$0" hello "$@"
)
(prn *command-line-args*)
./script.clj 1 2 3
("hello" "1" "2" "3")
Babashka ships with clojure.tools.cli
:
(require '[clojure.tools.cli :refer [parse-opts]])
(def cli-options
;; An option with a required argument
[["-p" "--port PORT" "Port number"
:default 80
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
["-h" "--help"]])
(:options (parse-opts *command-line-args* cli-options))
$ bb script.clj
{:port 80}
$ bb script.clj -h
{:port 80, :help true}
The environment variable BABASHKA_PRELOADS
allows to define code that will be
available in all subsequent usages of babashka.
BABASHKA_PRELOADS='(defn foo [x] (+ x 2))'
BABASHKA_PRELOADS=$BABASHKA_PRELOADS' (defn bar [x] (* x 2))'
export BABASHKA_PRELOADS
Note that you can concatenate multiple expressions. Now you can use these functions in babashka:
$ bb '(-> (foo *in*) bar)' <<< 1
6
You can also preload an entire file using load-file
:
export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")'
Note that *in*
is not available in preloads.
Start the socket REPL like this:
$ bb --socket-repl 1666
Babashka socket REPL started at localhost:1666
Now you can connect with your favorite socket REPL client:
$ 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
$
A socket REPL client for Emacs is inf-clojure.
You may use the conch
namespace for this. It maps to
me.raynes.conch.low-level
.
Example:
$ bb '
(def ws (conch/proc "python" "-m" "SimpleHTTPServer" "1777"))
(net/wait-for-it "localhost" 1777) (conch/destroy ws)'
Apart from future
for creating threads and the conch
namespace for creating
processes, you may use the async
namespace, which maps to clojure.core.async
, for asynchronous scripting. The following
example shows how to get first available value from two different processes:
bb '
(defn async-command [& args]
(async/thread (apply shell/sh "bash" "-c" args)))
(-> (async/alts!! [(async-command "sleep 2 && echo process 1")
(async-command "sleep 1 && echo process 2")])
first :out str/trim println)'
process 2
If you want to be able to use SSL to e.g. run (slurp "https://www.clojure.org")
you will need to add the location where
libsunec.so
or libsunec.dylib
is located to the java.library.path
Java
property. This library comes with most JVM installations, so you might already
have it on your machine. It is usually located in <JAVA_HOME>/jre/lib
or
<JAVA_HOME>/jre/<platform>/lib
. It is also bundled with GraalVM.
Example:
$ export BABASHKA_PRELOADS="(System/setProperty \"java.library.path\" \"$JAVA_HOME/jre/lib\")"
$ bb '(slurp "https://www.clojure.org")' | bb '(subs *in* 0 50)'
"<!doctype html><html itemscope=\"\" itemtype=\"http:/"
To work on Babashka itself make sure Git submodules are checked out.
$ git clone https://github.com/borkdude/babashka --recursive
To update later on:
$ git submodule update --recursive
You need Leiningen, and for building binaries you need GraalVM.
lein repl
will get you a standard REPL/nREPL connection. To work on tests use lein with-profiles +test repl
.
Test on the JVM:
script/test
Although this tool doesn't offer any benefit when running on the JVM, it is convenient for development.
Test the native version:
BABASHKA_TEST_ENV=native script/test
To build this project, set $GRAALVM_HOME
to the GraalVM distribution directory.
Then run:
script/compile
Here's a gallery of more useful examples. Do you have a useful example? PR welcome!
$ cat /tmp/test.txt
1 Hello
2 Clojure
3 Babashka
4 Goodbye
$ < /tmp/test.txt bb -io '(shuffle *in*)'
3 Babashka
2 Clojure
4 Goodbye
1 Hello
For converting JSON to EDN, see jet.
$ curl -s https://api.github.com/repos/borkdude/babashka/tags |
jet --from json --keywordize --to edn |
bb '(-> *in* first :name (subs 1))'
"0.0.4"
$ curl -s https://api.github.com/repos/borkdude/babashka/releases |
jet --from json --keywordize |
bb '(-> *in* first :assets)' |
bb '(some #(re-find #".*linux.*" (:browser_download_url %)) *in*)'
"https://github.com/borkdude/babashka/releases/download/v0.0.4/babashka-0.0.4-linux-amd64.zip"
Copyright © 2019 Michiel Borkent
Distributed under the EPL License. See LICENSE.
This project contains code from:
Can you improve this documentation? These fine people already did:
Michiel Borkent, Arne Brasseur & Peter StrömbergEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close