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)))
To turn all exported functions of a package into commands, use
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 !