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!

Staircase by Sven Read via Unsplash

Staircase by Sven Read via Unsplash

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

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:

OSCommand
Debian, Ubuntusudo apt install rlwrap
RedHat, CentOSsudo yum install rlwrap
Fedorasudo dnf install rlwrap
ArchLinuxsudo pacman -S rlwrap
macOSsudo 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:

  1. Start by downloading a recent release (or, if you’re feeling adventurous, download or git clone the latest code on the master branch)

  2. 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

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

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 #includes 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.