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 !