In this post, we’ll write a simple wrapper for a CLI tool to provide it with a REPL-like environment. Interested? Suspicious? Don’t know what a REPL is or where to start? Let’s dive in!
Background
If you’re already well-acquainted with REPLs, you can skip this section.
If you’re not familiar with the term, a REPL, or a read-eval-print loop is …
a simple, interactive computer programming environment that takes single user inputs (i.e., single expressions), evaluates (executes) them, and returns the result to the user
Many programming languages already provide such functionality, whether with the standard interpreter, or an additional command that is part of the development environment.
For example, python
is a script interpreter, but running it without any
parameters will present you with a REPL:
$ python
Python (...version, date...)
(...compilation, environment...)
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 6
>>> b = 7
>>> a * b
42
Similarly, tools like bc
(basic calculator) provide a REPL
directly, while some compilers and interpreters provide them via a separate
binary, e.g., the Glasgow Haskell Compiler (GHC) provides
ghci
, while Ruby provides irb
.
REPLs are very useful not just for those who are learning to use the language or the tool, but also for experienced practitioners who want to quickly try out an idea, without having to write a new file, run the compiler (with appropriate flags), examine the output, and then cleanup the experiment. A REPL frees you from all of that because it always provides a new, pristine environment, it’s easy to create and it automatically cleans everything up when you exit, and there are no traces of it left.
However, there are many tools that don’t provide a REPL of any sort, and maybe your favorite tool, or a command that you wrote yourself, doesn’t provide one either. Maybe it’s worthwhile to invest in building a complete REPL-like experience, but that’s probably a non-trivial amount of work. Is it possible to write something simple to get most of the benefits, without understanding the internals of the tool and modifying it?
Turns out, the answer is YES! Let’s go.
Minimum viable REPL
To start, we naturally need to select a tool that we want to wrap with a REPL. Ideally, this tool already has a way to execute different commands given different flags and command-line arguments as that simplifies the process, but if the only way it can work is via files, we can work with that, too! Note that as per above, the user expectation is that the user does not create or clean up any temporary files, build artifacts, error logs, etc., so if we need to do that as part of our wrapper, we have to take care to clean it up ourselves. We’ll come back to this later.
For simplicity, we’ll start by writing a wrapper for git
, because it’s
something we use quite a bit (for one, since this website is versioned in Git),
and when using Git, we typically have to run many commands in sequence, e.g.:
# typical Git session after creating a new blog post
$ git checkout -b my-new-blog-post
$ git add index.md
$ git add title-image.png
$ git commit
# write commit message
$ git push
In some cases, this may also involve git diff
, or git unstage
, git rebase
, or a host of other commands. It
gets tiring of having to type git
every time before each command, so how can
we avoid that?
Let’s write a small script:
#!/bin/bash -u
#
# Copyright 2020 Misha Brukman
# SPDX-License-Identifier: Apache-2.0
# https://misha.brukman.net/blog/2020/04/add-a-repl-to-any-cli-tool/
echo "This is the Git REPL; exit via Ctrl-D."
while true; do
echo -n "$(pwd)> git "
read command || break
git $command
done
# Add a blank line at the end for a clean prompt when exiting.
echo
That’s it! This works well enough for a number of basic use cases: for example,
you can now quickly go through the above command list without typing the git
prefix, while the command prompt will always remind you that you’re in
git
-mode.
Adding command-line history
One thing you’ll note is that this prompt is not quite as good as your standard shell prompt: while you can use Backspace to edit your command, there’s no command history that you can cycle via ↑ and ↓; instead, they generate strange codes on the screen:
This is the Git REPL; exit via Ctrl-D.
/my/path> git ^[[A^[[B
Turns out, functionality such as this is provided by the GNU Readline library—in fact, that’s the library that’s used by Bash—so if we want the same functionality, we need to use Readline or something similar. However, Readline is a library, and we’re trying to wrap a CLI tool in a shell script, so what do we do?
Turns out, there’s a program called rlwrap
which stands for
“readline wrapper” and it does exactly what we need: it wraps a CLI
tool while providing Readline functionality.
Installing rlwrap
To use rlwrap
, before proceeding, let’s install it on our system:
OS | Command |
---|---|
Debian, Ubuntu | sudo apt install rlwrap |
RedHat, CentOS | sudo yum install rlwrap |
Fedora | sudo dnf install rlwrap |
ArchLinux | sudo pacman -S rlwrap |
macOS | sudo brew install rlwrap |
Adjust accordingly for your own system.
Alternatively, if your system does not have this package provided, you can install it from source:
Start by downloading a recent release (or, if you’re feeling adventurous, download or
git clone
the latest code on themaster
branch)From the directory containing the
rlwrap
source, run the usual commands:$ ./configure && make && sudo make install
For more details and troubleshooting any issues, refer to the rlwrap
installation docs.
Using rlwrap
How does rlwrap
work? The catch is that in order to wrap your CLI tool, you
have to wrap your CLI tool with rlwrap
as follows:
$ rlwrap git-repl.sh
Otherwise, it doesn’t work. However, who’s going to remember to do this every time? We need to do this automatically.
Let’s update our script to automatically wrap itself with rlwrap
, but to
avoid an infinite loop, we’ll need to add a marker (like an environment
variable). Here’s the new version of our script:
#!/bin/bash -u
#
# Copyright 2020 Misha Brukman
# SPDX-License-Identifier: Apache-2.0
# https://misha.brukman.net/blog/2020/04/add-a-repl-to-any-cli-tool/
if ! [ -n "${REPL_USING_RLWRAP:-}" ]; then
# Use `rlwrap` if available.
if which rlwrap > /dev/null 2>&1; then
exec env REPL_USING_RLWRAP=1 rlwrap "$0" "$@"
else
echo 'Install `rlwrap` and re-run this script to get command history.'
fi
fi
echo "This is the Git REPL; exit via Ctrl-D."
while true; do
echo -n "$(pwd)> git "
read command || break
git $command
done
# Add a blank line at the end for a clean prompt when exiting.
echo
Note that this is a simple script which assumes you’re running it as:
$ ./git-repl-rlwrap.sh
If you start it via:
$ bash git-repl-rlwrap.sh
it will not work. It can be extended to handle this; consider it an exercise for the reader.
Now we have a fully-functional script, with command history, and you can easily
swap out the git
prompt and command for any other tool, and it will work just
fine.
But why stop there? Let’s add ability to run non-Git commands, as well as some built-ins to simplify our REPL even further.
Escape the REPL with shell commands
One thing you may have noticed while testing this script is that since we
hard-code the git
prefix to all of our commands, we cannot run any other
command, such as ls
or cd
or our favorite $EDITOR
to change a file—we
have to exit the REPL to do any of those things, which is disruptive.
Let’s add an ability to run arbitrary shell commands. The rule we’re going to
use is that if they’re prefixed with !
, then it’s a shell command; otherwise,
we’ll run it through git. So to run ls -l
, we’ll type !ls -l
, and so on.
This is a very simple update to our script; here’s the latest version:
#!/bin/bash -u
#
# Copyright 2020 Misha Brukman
# SPDX-License-Identifier: Apache-2.0
# https://misha.brukman.net/blog/2020/04/add-a-repl-to-any-cli-tool/
if ! [ -n "${REPL_USING_RLWRAP:-}" ]; then
# Use `rlwrap` if available.
if which rlwrap > /dev/null 2>&1; then
exec env REPL_USING_RLWRAP=1 rlwrap "$0" "$@"
else
echo 'Install `rlwrap` and re-run this script to get command history.'
fi
fi
echo "This is the Git REPL; exit via Ctrl-D."
while true; do
echo -n "$(pwd)> git "
read command || break
# A command with `!` prefix executes directly via the shell, not `git`.
if [ "${command:0:1}" = '!' ]; then
${command:1}
else
git $command
fi
done
# Add a blank line at the end for a clean prompt when exiting.
echo
And there you have it!
If you want to add internal settings or other functionality, consider adding it
via a :
prefix on commands, which you can easily add by following the same
pattern as above.
You can extend it to run C or C++ compilers which don’t have REPLs by
outputting a line into a file, surrounded by standard #include
s as well as an
int main()
function declaration, and then running that through the relevant
compiler. This could be useful for testing expressions or even complex template
declarations and usage.
Now you’re equipped to build custom REPLs for any CLI tool of your choosing! Let me know what you end up using it for; I’d love to hear more real-world use cases.