When I started dabbling in CL, I tried to build a readline application to see how it goes. I found cl-readline (I'm only the new maintainer) and it went smoothly. So I built a second and a third app, and found many things to refactor and provide out of the box: now comes replic.

It comes as a library (now in Quicklisp, since 2018-01) and as an executable. The library does the following for you:

  • it builds the repl loop, catches a C-c, a C-d, errors,
  • it asks confirmation to quit,
  • it asks depending on a .conf and a lispy config file,
  • it reads parameters from a config file,
  • it prints the help of all or one command (with optional highlighting),
  • and more importantly it handles the completion of commands and of their arguments.

For example, instead of this “repl” loop:

  (handler-case
      (do ((i 0 (1+ i))
           (text "")
           (verb "")
           (function nil)
           (variable nil)
           (args ""))
          ((string= "quit" (str:trim text)))

        (handler-case
            (setf text
                  (rl:readline :prompt (prompt)
                               :add-history t))
          (#+sbcl sb-sys:interactive-interrupt ()
                  (progn
                    (when (confirm)
                      (uiop:quit)))))

        (if (string= text "NIL")
            ;; that's a C-d, a blank input is just "".
            (when (confirm)
              (uiop:quit)))

        (unless (str:blank? text)
          (setf verb (first (str:words text)))
          (setf function (if (replic.completion:is-function verb)
                             ;; might do better than this or.
                             (replic.completion:get-function verb)))
          (setf variable (if (replic.completion:is-variable verb)
                             (replic.completion:get-variable verb)))
          (setf args (rest (str:words text)))


          (if (and verb function)
              (handler-case
                  ;; Call the function.
                  (apply function args)
                (#+sbcl sb-sys:interactive-interrupt (c)
                        (declare (ignore c))
                        (terpri))
                (error (c) (format t "Error: ~a~&" c)))

              (if variable
                  (format t "~a~&" (symbol-value variable))
                  (format t "No command or variable bound to ~a~&" verb)))

          (finish-output)

          (when (and *history*
                     *write-history*)
            (rl:write-history "/tmp/readline_history"))
          ))

    (error (c)
      (format t "~&Unknown error: ~&~a~&" c)))

you call:

(replic:repl)

To turn all exported functions of a package into commands, use

(replic:functions-to-commands :my-package)

and you can find them into the readline app.

Setting the completion of commands is easy, we use (replic.completion:add-completion "my-function" <list-or-lambda>. For example:

(in-package :replic.user)

(defparameter *names* '()
  "List of names (string) given to `hello`. Will be autocompleted by `goodbye`.")

(defun hello (name)
  "Takes only one argument. Adds the given name to the global
  `*names*` variable, used to complete arguments of `goodbye`.
  "
  (format t "hello ~a~&" name)
  (push name *names*))

(defun goodbye (name)
  "Says goodbye to name, where `name` should be completed from what was given to `hello`."
  (format t "goodbye ~a~&" name))

(replic.completion:add-completion "goodbye" (lambda () *names*))

(export '(hello goodbye))

This example can be used with the executable. What it does is read your code from a lisp file (~/.replic.lisp or an argument on the command line) and it turns the exported functions into commands, for which we can specify custom completion.

For more details, see the readme.


I use this currently in three apps of mine (like cl-torrents). It's simple. It could be more: it could infer the arguments’ type, do fuzzy completion, maybe integrate a Lisp editor (Lem) or a lispy shell (shcl), separate the commands in apps, expose hooks, have a set of built-in shell related utilities, highlight the input line, it could be web-based,…

For now it's going smoothly.

I'll finish by recalling that it's amazing to be able to ship self-contained executables to users !