A shell nREPL client for boot nREPL and other nREPls.
bootycall
is based on the [Python nREPL client implementation] (https://github.com/cemerick/nrepl-python-client) by cemerick.
The main motivation for this script was to make Clojure code callable from the shell by using [nREPL] (https://github.com/clojure/tools.nrepl). In particular, [boot] (https://github.com/boot-clj/boot) tasks and CLI tasks can be called very fast as the JVM has not to start up for every call.
In the following snippet, bcall
makes a call to the task fire
(see [here] (http://www.braveclojure.com/appendix-b/)) by sending the call to a running boot nREPL process.
This takes about 0.21s (on a mid-2012 Macbook Pro):
% time bcall -v fire --thing pants -p
My pants are on fire!
bcall fire --thing pants -p 0.21s user 0.03s system 92% cpu 0.259 tota
Directly calling boot
takes around 7s.
% time boot fire --thing pants -p
My pants are on fire!
boot fire --thing pants -p 6.99s user 0.27s system 255% cpu 2.848 total
The general idea is to make writing Clojure code for the shell more practical by avoiding the JVM startup time that is needed with every call.
And even if the final Clojure "Shell" script is called directly, bootycall
can be used during development for testing.
You can either clone this repository and call the Python script bcall.py
directly or you can install bootycall
via pip
:
pip install bootycall
Start an nREPL either via Leiningen (lein repl
) or via Boot (boot repl
).
You should see some message that an nREPL server has been started with a nREPL
URL of the form nrepl://127.0.0.1:<port>
.
With bootycall
you can evaluate Clojure code within the REPL context from you shell.
Let's define the classic Fibonacci function (replace <port>
with the port
number of your nREPL):
bcall -p <port> -f << EOF
(defn fib [n]
(if (<= n 1)
1
(+ (-> n dec fib) (-> n dec dec fib))))
EOF
As answer you should get the output produced by the REPL (e.g. #'boot.user/fib
in case you used boot
).
-p
tells bootycall
the port to connect to and with -f
(form) we specify that we want to send one (or more) forms to the nREPL, the definition of fib
in our example.
When we would leave -f
out, then bootycall
assumes that we want to call a boot
(CLI) task (see below).
The Clojure code is read from stdin
(the keyboard in the shell) until EOF
is read.
We can now call fib
:
bcall -p <port> -f "(fib 10)"
The answer should be 89
. Note that this time we passed the form directly as an argument to bootycall
.
Conversely, when we provide no code bootycall
reads it from stdin
as seen above.
When you start an nREPL service then this will create the file .nrepl-port
in
the current working directory.
bootycall
uses this file when you don't specify any port via -p
.
Hence, when you execute the bootycall
in the same directory as the nREPL
service is running you don't need to specify the port.
Alternatively, you can define the URL to the nREPL via the environment variable
NREPL_URL
:
export NREPL_URL="nrepl://127.0.0.1:<port>"
bcall -f "(map inc (range 10))"
(1 2 3 4 5 6 7 8 9 10)
Assume we define the following boot task (taken from [Clojure for the Brave and True by Daniel Higginbotham] (http://www.braveclojure.com/appendix-b/)):
(deftask fire
"Announces that something is on fire"
[t thing THING str "The thing that's on fire"
p pluralize bool "Whether to pluralize"]
(let [verb (if pluralize "are" "is")]
(println "My" thing verb "on fire!")))
With boot
we could call this task via:
boot fire -t Pants -p
My Pants are on fire!
Which takes some time due to the startup time needed by the JVM.
With bootycall
you can use exeucte the task faster:
% bcall fire -t Pants -p
My Pants are on fire!
nil
Note that -f
is not needed here as we don't pass a form to the nREPL but make a call to boot
.
Also note that the second output is nil
which is the return value of the task.
More on the different types of outputs in the next section.
When evaluating via the nREPL, there are two types of output:
The stuff written to standard output (e.g. via (println ...)
) and the return value generated by the evaluation of the form.
(There is also stuff written to standard error which is passed to stderr
by bootycall
.)
For instance, consider the following call to bcall
with a Fibonacci function that prints something to the console:
% bcall -f << EOF
(defn fib [n]
(println (str "(fib " n ")"))
(if (<= n 1)
1
(+ (-> n dec fib) (-> n dec dec fib))))
EOF
Executing it produces:
% bcall -f -o "(fib 3)"
(fib 3)
(fib 2)
(fib 1)
(fib 0)
(fib 1)
3
All lines except for the last one are (console) output and the last line is the return value of fib
.
If we are not interested in the output but only in the values, we can use -o
to supress the output:
% bcall -f -o "(fib 3)"
3
Likewise, to supress the value, we use -v
:
% bcall -f -v "(fib 3)"
(fib 3)
(fib 2)
(fib 1)
(fib 0)
(fib 1)
Note that passing multiple forms to bootycall
produces multiple values:
% bcall -f -o "(fib 3) (fib 4) (fib 5)"
3
5
8
bootycall
can also redirect (console) output and values to different file descriptors.
The following example redirects the former to the file output
and the latter to the file values
:
% exec {OUTPUT_FD} > output
% exec {VALUES_FD} > values
% bcall -f -O ${OUTPUT_FD} -V ${VALUES_FD} "(fib 3) (fib 4) (fib 5)"
% cat values
3
5
8
When you start the nREPL service and bootycall
from the same directory, then both share the same working directory.
In particular, relative paths in each point to the same files.
However, when you call bcall
from a different directly, then their relative paths to files are different.
% ls
a.txt
% cat a.txt
I am a.txt and contain some text.
% bcall -f '(slurp "a.txt")'
java.io.FileNotFoundException: a.txt (No such file or directory)
We can avoid this problem by calling bootycall
in working directory of the nREPL service or by using absolute paths:
% bcall -f "(slurp \"${PWD}/a.txt\")"
"I am a.txt and contain some text.\n"
Copyright ©2016 Christopher Auer
Distributed under the MIT License. Please see the LICENSE file at the top level of this repo.