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-11-07 11:46:14 +00:00
A Clojure [babushka ](https://en.wikipedia.org/wiki/Headscarf ) for the grey areas of Bash.
2019-08-12 12:42:45 +00:00
## 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
2019-11-03 22:13:06 +00:00
The sweet spot for babashka is executing Clojure snippets or scripts in the same
2019-11-07 11:46:14 +00:00
space where you would use Bash.
2019-08-15 09:05:59 +00:00
2019-11-07 11:25:51 +00:00
As one user described it:
2019-11-07 11:46:14 +00:00
> I’ m quite at home in Bash most of the time, but there’ s a substantial grey area of things that are too complicated to be simple in bash, but too simple to be worth writing a clj/s script for. Babashka really seems to hit the sweet spot for those cases.
2019-11-07 11:25:51 +00:00
2019-11-03 20:28:16 +00:00
Goals:
2019-11-03 22:14:57 +00:00
* Fast startup / low latency. This is achieved by compiling to native using [GraalVM ](https://github.com/oracle/graal ).
2019-11-03 22:18:32 +00:00
* Familiarity and portability. Keep migration barriers between bash and Clojure as low as possible by:
- Gradually introducing Clojure expressions to existing bash scripts
2019-11-03 22:19:30 +00:00
- Scripts written in babashka should also be able to run on the JVM without major changes.
2019-11-03 20:28:16 +00:00
* Multi-threading support similar to Clojure on the JVM
* Batteries included (clojure.tools.cli, core.async, ...)
Non-goals:
* Performance
* Provide a mixed Clojure/bash DSL (see portability).
* Replace existing shells. Babashka is a tool you can use inside existing shells like bash and it is designed to play well with them. It does not aim to replace them.
2019-08-17 22:31:39 +00:00
2019-09-08 21:31:24 +00:00
Reasons why babashka may not be the right fit for your use case:
2019-08-15 09:05:59 +00:00
2019-09-08 21:31:24 +00:00
- It uses [sci ](https://github.com/borkdude/sci ) for interpreting Clojure. Sci
2019-11-03 20:28:16 +00:00
implements only a subset of Clojure and is not as performant as compiled code.
2019-09-08 21:31:24 +00:00
- External libraries are not available (although you may use `load-file` for
loading external scripts).
2019-10-07 21:11:49 +00:00
Read more about the differences with Clojure [here ](#differences-with-clojure ).
2019-10-07 21:09:35 +00:00
2019-09-08 21:31:24 +00:00
## Status
Experimental. Breaking changes are expected to happen at this phase.
## Examples
2019-08-17 22:31:39 +00:00
``` shellsession
2019-09-08 21:31:24 +00:00
$ ls | bb -i '*in*'
["LICENSE" "README.md" "bb" "doc" "pom.xml" "project.clj" "reflection.json" "resources" "script" "src" "target" "test"]
2019-08-17 22:31:39 +00:00
2019-09-08 21:31:24 +00:00
$ ls | bb -i '(count *in* )'
12
2019-08-17 22:31:39 +00:00
2019-09-08 21:31:24 +00:00
$ bb '(vec (dedupe *in* ))' <<< '[1 1 1 1 2]'
[1 2]
2019-08-15 09:16:15 +00:00
2019-09-08 21:31:24 +00:00
$ bb '(filterv :foo *in* )' <<< '[{:foo 1} {:bar 2}]'
[{:foo 1}]
2019-08-18 05:30:29 +00:00
2019-09-08 21:31:24 +00:00
$ bb '(#(+ %1 %2 %3) 1 2 *in* )' << < 3
6
2019-08-09 12:51:42 +00:00
2019-09-08 21:31:24 +00:00
$ 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 ](#gallery ).
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-08 21:31:24 +00:00
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.
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-09-08 21:07:58 +00:00
- [`clojure.tools.cli` ](https://github.com/clojure/tools.cli ) aliased as `tools.cli`
2019-11-11 20:14:30 +00:00
- [`clojure.data.csv` ](https://github.com/clojure/data.csv ) aliased as `csv`
2019-08-14 11:38:39 +00:00
From Java the following is available:
2019-08-13 22:19:15 +00:00
2019-10-14 09:24:37 +00:00
- `Integer` :
- static methods: `parseInt`
- `File` :
- static methods: `createTempFile`
- instance methods: `.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` :
- static methods: `exit` , `getProperty` , `setProperty` , `getProperties` , `getenv`
- `Thread` :
- static methods: `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:
2019-10-14 09:24:37 +00:00
- `wait/wait-for-port` . Usage:
2019-09-04 12:37:07 +00:00
``` clojure
2019-10-14 09:24:37 +00:00
(wait/wait-for-port "localhost" 8080)
(wait/wait-for-port "localhost" 8080 {:timeout 1000 :pause 1000})
2019-09-04 12:37:07 +00:00
```
2019-10-14 09:24:37 +00:00
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.
- `wait/wait-for-path` . Usage:
``` clojure
(wait/wait-for-path "/tmp/wait-path-test")
(wait/wait-for-path "/tmp/wait-path-test" {:timeout 1000 :pause 1000})
```
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.
2019-09-04 12:37:07 +00:00
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-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-09-11 21:37:25 +00:00
## Parsing command line arguments
Babashka ships with `clojure.tools.cli` :
``` clojure
(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))
```
``` shellsession
$ bb script.clj
{:port 80}
$ bb script.clj -h
{:port 80, :help true}
```
2019-10-26 21:53:10 +00:00
## Reader conditionals
Babashka supports reader conditionals using the `:bb` feature:
``` clojure
2019-10-26 22:01:32 +00:00
$ cat example.clj
2019-10-26 21:53:10 +00:00
#?(:clj (in-ns 'foo) :bb (println "babashka doesn't support in-ns yet!"))
$ ./bb example.clj
babashka doesn't support in-ns yet!
```
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-10-07 21:11:49 +00:00
## Differences with Clojure
2019-10-07 21:09:35 +00:00
Babashka is implemented using the [Small Clojure
Interpreter](https://github.com/borkdude/sci). This means that a snippet or
script is not compiled to JVM bytecode, but executed form by form by a runtime
which implements a subset of Clojure. Babashka is compiled to a native binary
using [GraalVM ](https://github.com/oracle/graal ). It comes with a selection of
built-in namespaces and functions from Clojure and other useful libraries. The
data types (numbers, strings, persistent collections) are the
same. Multi-threading is supported (`pmap`, `future` ).
2019-10-07 21:11:49 +00:00
Differences with Clojure:
2019-10-07 21:09:35 +00:00
- No user defined namespaces. Since this tool focuses on snippets and small
scripts, there hasn't been a need to implement it yet.
- There is no `ns` macro for the same reason as above.
- No first class vars. Note that you can define and redefine global values with
`def` / `defn` , but there is no `var` indirection.
2019-10-07 21:22:11 +00:00
- Java classes and interop are not available. For a selection of classes we mimic constructors and interop by having
2019-10-07 21:09:35 +00:00
functions like `Exception.` and `.getCanonicalPath` .
- Only the `clojure.core` , `clojure.set` and `clojure.string` namespaces are
available from Clojure.
- There is no classpath and no support for loading code from Maven/Clojars
dependencies. However, you can use `load-file` to load external code from
disk.
2019-10-07 21:15:22 +00:00
- `require` does not load files; it only provides a way to create different
aliases for included namespaces, which makes it easier to make scripts portable between the JVM and babashka.
2019-10-07 21:09:35 +00:00
- Interpretation comes with overhead. Therefore tight loops are likely slower
than in Clojure on the JVM.
- No support for unboxed types.
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-10-14 09:24:37 +00:00
Test on the JVM (for development):
2019-08-09 12:51:42 +00:00
script/test
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-11-08 19:23:43 +00:00
### Delete a list of files returned by a Unix command
```
find . | grep conflict | bb -i '(doseq [f *in* ] (.delete (io/file f)))'
```
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-11-03 20:38:06 +00:00
### View download statistics from Clojars
``` shellsession
2019-11-03 21:43:38 +00:00
$ curl https://clojars.org/stats/all.edn |
2019-11-03 20:39:36 +00:00
bb -o '(for [[[group art] counts] *in* ] (str (reduce + (vals counts)) " " group "/" art))' |
sort -rn |
less
2019-11-03 21:43:01 +00:00
14113842 clojure-complete/clojure-complete
9065525 clj-time/clj-time
8504122 cheshire/cheshire
...
2019-11-03 20:38:06 +00:00
```
2019-11-03 20:28:16 +00:00
## Thanks
2019-11-03 20:33:01 +00:00
- [adgoji ](https://www.adgoji.com/ ) for financial support
2019-11-03 20:28:16 +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.