2019-08-13 11:43:15 +00:00
# babashka
2019-08-09 12:51:42 +00:00
[](https://circleci.com/gh/borkdude/babashka/tree/master)
[](https://clojars.org/borkdude/babashka)
[](https://cljdoc.org/d/borkdude/babashka/CURRENT)
2019-08-12 12:42:45 +00:00
A sprinkle of Clojure for the command line.
## Quickstart
``` shellsession
$ bash < (curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install)
2019-08-18 21:17:20 +00:00
$ ls | bb --time -i '(filter #(-> % io/file .isDirectory) *in* )'
2019-08-18 10:44:51 +00:00
("doc" "resources" "sci" "script" "src" "target" "test")
2019-08-18 21:17:20 +00:00
bb took 4ms.
2019-08-12 12:42:45 +00:00
```
## Rationale
If you're a bash expert, you probably don't need this. But for those of us who
2019-08-15 09:18:25 +00:00
can use a bit of Clojure in their shell scripts, it may be useful.
2019-08-09 12:51:42 +00:00
2019-08-18 05:30:29 +00:00
Babashka runs as a [GraalVM ](https://github.com/oracle/graal ) binary which
results in fast startup times:
2019-08-15 09:16:15 +00:00
2019-08-15 09:05:59 +00:00
``` shellsession
2019-08-17 22:31:39 +00:00
$ time clojure -e "(+ 1 2 3)"
6
clojure -e "(+ 1 2 3)" 3.29s user 0.32s system 99% cpu 3.638 total
2019-08-15 09:05:59 +00:00
2019-08-17 22:40:01 +00:00
$ time planck -e '(+ 1 2 3)'
2019-08-17 22:31:39 +00:00
6
plk -e '(+ 1 2 3)' 1.34s user 0.16s system 127% cpu 1.172 total
$ time bb '(+ 1 2 3)'
6
bb '(+ 1 2 3)' 0.01s user 0.01s system 37% cpu 0.046 total
2019-08-15 09:05:59 +00:00
```
2019-08-17 22:31:39 +00:00
It uses [sci ](https://github.com/borkdude/sci ) for interpreting Clojure. A
trade-off is that [sci ](https://github.com/borkdude/sci ) implements only a
subset of Clojure. Also, execution time may be slower than Clojure on the JVM or
(self-hosted) ClojureScript for more CPU-intensive calculations like:
``` shellsession
(last (take 1000000 (repeatedly #(+ 1 2 3))))
```
This would take 5 seconds using babashka, around half a second using self-hosted
ClojureScript and around 200ms in Clojure on the JVM.
So the sweet spot for babashka is executing tasks from the command line where
fast startup time is preferred, in the same space where you would use bash.
2019-08-15 09:16:15 +00:00
2019-08-18 05:30:29 +00:00
Where it can, babashka calls the regular implementation of Clojure on the JVM
2019-08-18 21:20:17 +00:00
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.
2019-08-18 05:30:29 +00:00
2019-08-09 12:51:42 +00:00
## Status
2019-08-10 16:50:48 +00:00
Experimental. Breaking changes are expected to happen at this phase.
2019-08-09 12:51:42 +00:00
2019-08-09 14:02:08 +00:00
## Installation
2019-08-11 07:27:45 +00:00
### Brew
2019-08-09 14:02:08 +00:00
Linux and macOS binaries are provided via brew.
Install:
brew install borkdude/brew/babashka
Upgrade:
brew upgrade babashka
2019-08-11 07:27:45 +00:00
### Installer script
Install via the installer script:
``` shellsession
$ 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:
``` shellsession
$ bash < (curl -s https://raw.githubusercontent.com/borkdude/babashka/master/install) /tmp
```
### Download
2019-08-09 14:02:08 +00:00
You may also download a binary from [Github ](https://github.com/borkdude/babashka/releases ).
2019-08-09 13:48:54 +00:00
## Usage
2019-08-09 12:51:42 +00:00
2019-08-09 17:58:11 +00:00
``` shellsession
2019-09-06 14:40:50 +00:00
Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] ( -e < expression > | -f < file > | --socket-repl [< host > :]< port > )
2019-08-18 10:33:54 +00:00
Options:
2019-08-09 17:58:11 +00:00
2019-08-31 18:21:39 +00:00
--help, -h or -?: print this help text.
2019-08-18 10:33:54 +00:00
--version: print the current version of babashka.
2019-08-29 11:57:39 +00:00
-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.
2019-09-06 14:40:50 +00:00
-e, --eval < expression > : evaluate an expression
-f, --file < path > : evaluate a file
2019-08-31 18:21:39 +00:00
--socket-repl: start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
2019-08-18 10:33:54 +00:00
--time: print execution time before exiting.
2019-09-06 14:40:50 +00:00
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* .
2019-08-18 10:33:54 +00:00
```
2019-08-14 11:38:39 +00:00
The `clojure.core` functions are accessible without a namespace alias.
2019-09-04 12:37:07 +00:00
The following namespaces are required by default and only available
2019-08-18 10:33:54 +00:00
through the aliases. If not all vars are available, they are enumerated
explicitly.
2019-08-14 11:38:39 +00:00
- `clojure.string` aliased as `str`
- `clojure.set` aliased as `set`
2019-08-18 10:33:54 +00:00
- `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`
2019-09-07 08:43:53 +00:00
- [`clojure.core.async` ](https://clojure.github.io/core.async/ ) aliased as
`async` . The `alt` and `go` macros are not available but `alts!!` does work as
it is a function.
2019-09-04 12:37:07 +00:00
- [`me.raynes.conch.low-level` ](https://github.com/clj-commons/conch#low-level-usage )
aliased as `conch`
2019-08-14 11:38:39 +00:00
From Java the following is available:
2019-08-13 22:19:15 +00:00
2019-08-18 10:33:54 +00:00
- `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` .
2019-09-04 12:37:07 +00:00
- `System` : `exit` , `getProperty` , `setProperty` , `getProperties` , `getenv`
- `Thread` : `sleep`
2019-08-15 04:28:00 +00:00
Special vars:
2019-08-29 12:01:40 +00:00
- `*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.
2019-08-15 04:28:00 +00:00
- `*command-line-args*` : contain the command line args
2019-08-10 16:50:48 +00:00
2019-09-04 12:37:07 +00:00
Additionally, babashka adds the following functions:
- `net/wait-for-it` . Usage:
``` clojure
(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.
2019-09-07 08:43:53 +00:00
- `sig/pipe-signal-received?` . Usage:
``` clojure
(sig/pipe-signal-received?)
```
Returns true if `PIPE` signal was received. Example:
``` shellsession
$ bb '((fn [x] (println x) (when (not (sig/pipe-signal-received?)) (recur (inc x)))) 0)' | head -n2
1
2
```
2019-09-04 12:37:07 +00:00
## Examples
2019-08-09 12:51:42 +00:00
``` shellsession
2019-08-10 16:50:48 +00:00
$ ls | bb -i '*in*'
2019-08-09 15:38:26 +00:00
["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "reflection.json" "resources" "script" "src" "target" "test"]
2019-08-10 16:50:48 +00:00
$ ls | bb -i '(count *in* )'
12
2019-08-09 15:38:26 +00:00
2019-08-10 07:52:21 +00:00
$ bb '(vec (dedupe *in* ))' <<< '[1 1 1 1 2]'
2019-08-09 12:51:42 +00:00
[1 2]
2019-08-10 16:50:48 +00:00
$ bb '(filterv :foo *in* )' <<< '[{:foo 1} {:bar 2}]'
[{:foo 1}]
2019-08-09 12:51:42 +00:00
2019-08-10 16:50:48 +00:00
$ bb '(#(+ %1 %2 %3) 1 2 *in* )' << < 3
2019-08-09 15:41:33 +00:00
6
2019-08-09 16:17:28 +00:00
2019-08-10 16:50:48 +00:00
$ ls | bb -i '(filterv #(re-find #"reflection" %) *in* )'
2019-08-09 15:38:26 +00:00
["reflection.json"]
2019-08-14 11:38:39 +00:00
$ bb '(run! #(shell/sh "touch" (str "/tmp/test/" %)) (range 100))'
2019-08-13 22:19:15 +00:00
$ ls /tmp/test | bb -i '*in*'
["0" "1" "10" "11" "12" "13" "14" "15" "16" "17" "18" "19" "2" "20" "21" ...]
2019-08-29 11:57:39 +00:00
$ bb -O '(repeat "dude")' | bb --stream '(str *in* "rino")' | bb -I '(take 3 *in* )'
("duderino" "duderino" "duderino")
2019-08-13 22:19:15 +00:00
```
2019-08-10 20:52:30 +00:00
More examples can be found in the [gallery ](#gallery ).
2019-08-09 21:08:49 +00:00
2019-08-14 11:38:39 +00:00
## Running a file
2019-08-15 10:55:58 +00:00
Scripts may be executed from a file using `-f` or `--file` :
2019-08-14 11:38:39 +00:00
``` shellsession
2019-08-15 07:08:30 +00:00
bb -f download_html.clj
2019-08-14 11:38:39 +00:00
```
2019-08-18 05:34:47 +00:00
Files can also be loaded inline using `load-file` :
``` shellsession
bb '(load-file "script.clj")'
```
2019-08-14 11:38:39 +00:00
Using `bb` with a shebang also works:
2019-08-15 07:04:14 +00:00
``` clojure
2019-09-07 08:49:31 +00:00
#!/usr/bin/env bb
2019-08-15 04:28:00 +00:00
(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]
2019-08-15 07:08:30 +00:00
(println "Writing file:" file)
2019-08-15 04:28:00 +00:00
(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)
2019-08-14 11:38:39 +00:00
```
2019-08-15 08:41:37 +00:00
``` shellsession
$ ./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
```
2019-09-07 08:49:31 +00:00
If `/usr/bin/env` doesn't work for you, you can use the following workaround:
``` shellsession
$ cat script.clj
#!/bin/sh
#_(
"exec" "bb" "$0" hello "$@"
)
(prn *command-line-args* )
./script.clj 1 2 3
("hello" "1" "2" "3")
```
2019-08-18 06:40:28 +00:00
## Preloads
The environment variable `BABASHKA_PRELOADS` allows to define code that will be
available in all subsequent usages of babashka.
``` shellsession
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:
``` shellsession
$ bb '(-> (foo *in* ) bar)' << < 1
6
```
You can also preload an entire file using `load-file` :
``` shellsession
export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")'
```
Note that `*in*` is not available in preloads.
2019-08-31 18:17:36 +00:00
## Socket REPL
Start the socket REPL like this:
``` shellsession
$ bb --socket-repl 1666
Babashka socket REPL started at localhost:1666
```
Now you can connect with your favorite socket REPL client:
``` shellsession
$ rlwrap nc 127.0.0.1 1666
2019-08-31 19:22:31 +00:00
Babashka v0.0.14 REPL.
2019-08-31 18:17:36 +00:00
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 ](https://github.com/clojure-emacs/inf-clojure ).
2019-09-04 12:37:07 +00:00
## Spawning and killing a process
You may use the `conch` namespace for this. It maps to
[`me.raynes.conch.low-level` ](https://github.com/clj-commons/conch#low-level-usage ).
Example:
``` clojure
$ bb '
(def ws (conch/proc "python" "-m" "SimpleHTTPServer" "1777"))
(net/wait-for-it "localhost" 1777) (conch/destroy ws)'
```
2019-09-07 08:43:53 +00:00
## Async
Apart from `future` for creating threads and the `conch` namespace for creating
2019-09-07 09:00:47 +00:00
processes, you may use the `async` namespace, which maps to `clojure.core.async` , for asynchronous scripting. The following
2019-09-07 08:43:53 +00:00
example shows how to get first available value from two different processes:
``` clojure
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
```
2019-08-17 15:38:24 +00:00
## Enabling SSL
2019-08-17 20:19:46 +00:00
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.
2019-08-17 15:38:24 +00:00
2019-08-17 20:19:46 +00:00
Example:
2019-08-17 15:38:24 +00:00
``` shellsession
2019-08-18 06:40:28 +00:00
$ export BABASHKA_PRELOADS="(System/setProperty \"java.library.path\" \"$JAVA_HOME/jre/lib\")"
$ bb '(slurp "https://www.clojure.org")' | bb '(subs *in* 0 50)'
2019-08-17 20:19:46 +00:00
"<!doctype html> < html itemscope = \"\" itemtype= \"http:/"
```
2019-08-17 15:38:24 +00:00
2019-09-06 13:31:54 +00:00
## Developing Babashka
To work on Babashka itself make sure Git submodules are checked out.
``` shellsession
$ git clone https://github.com/borkdude/babashka --recursive
```
To update later on:
``` shellsession
$ git submodule update --recursive
```
You need [Leiningen ](https://leiningen.org/ ), and for building binaries you need GraalVM.
### REPL
`lein repl` will get you a standard REPL/nREPL connection. To work on tests use `lein with-profiles +test repl` .
### Test
2019-08-09 12:51:42 +00:00
2019-08-09 18:01:12 +00:00
Test on the JVM:
2019-08-09 12:51:42 +00:00
script/test
2019-08-09 18:01:12 +00:00
Although this tool doesn't offer any benefit when running on the JVM, it is
convenient for development.
2019-08-09 12:51:42 +00:00
Test the native version:
BABASHKA_TEST_ENV=native script/test
2019-09-06 13:31:54 +00:00
### Build
2019-08-17 20:27:13 +00:00
To build this project, set `$GRAALVM_HOME` to the GraalVM distribution directory.
Then run:
2019-08-09 12:51:42 +00:00
script/compile
2019-08-13 07:20:48 +00:00
## Related projects
- [planck ](https://planck-repl.org/ )
- [joker ](https://github.com/candid82/joker )
- [closh ](https://github.com/dundalek/closh )
- [lumo ](https://github.com/anmonteiro/lumo )
2019-08-10 16:50:48 +00:00
## Gallery
Here's a gallery of more useful examples. Do you have a useful example? PR
welcome!
2019-08-10 20:52:30 +00:00
### Shuffle the lines of a file
``` shellsession
$ cat /tmp/test.txt
1 Hello
2 Clojure
2019-08-13 11:43:15 +00:00
3 Babashka
2019-08-10 20:52:30 +00:00
4 Goodbye
$ < /tmp/test.txt bb -io '(shuffle *in* )'
2019-08-13 11:43:15 +00:00
3 Babashka
2019-08-10 20:52:30 +00:00
2 Clojure
4 Goodbye
1 Hello
```
2019-08-10 16:50:48 +00:00
### Fetch latest Github release tag
For converting JSON to EDN, see [jet ](https://github.com/borkdude/jet ).
``` shellsession
2019-08-11 07:05:25 +00:00
$ curl -s https://api.github.com/repos/borkdude/babashka/tags |
2019-08-12 07:28:37 +00:00
jet --from json --keywordize --to edn |
bb '(-> *in* first :name (subs 1))'
2019-08-10 18:34:24 +00:00
"0.0.4"
```
2019-08-10 18:52:13 +00:00
### Get latest OS-specific download url from Github
2019-08-10 18:34:24 +00:00
``` shellsession
2019-08-11 07:05:25 +00:00
$ curl -s https://api.github.com/repos/borkdude/babashka/releases |
2019-08-12 07:28:37 +00:00
jet --from json --keywordize |
bb '(-> *in* first :assets)' |
bb '(some #(re-find #".*linux.*" (:browser_download_url %)) *in* )'
2019-08-10 18:52:13 +00:00
"https://github.com/borkdude/babashka/releases/download/v0.0.4/babashka-0.0.4-linux-amd64.zip"
2019-08-10 16:50:48 +00:00
```
2019-08-09 12:51:42 +00:00
## License
Copyright © 2019 Michiel Borkent
2019-09-04 12:37:07 +00:00
Distributed under the EPL License. See LICENSE.
This project contains code from:
- Clojure, which is licensed under the same EPL License.
- [conch ](https://github.com/clj-commons/conch ), which is licensed under the
same EPL License.