Scripting, Common Lisp style
There are various contortions for getting your Common Lisp started in a shell script and running some of your code. In the end, you're still falling back on writing Lisp like you're writing anything else: write, run, stop, repeat.
If only we could do this:
(defun my-command (x) (format t "If this were a real command, we would use ~A responsibly~%" x)) ;; Make a wrapper to call this in our running Lisp! (make-command "my-command" 'my-command)
$ my-command "foo" If this were a real command, we would use foo responsibly. $
Well, of course, that's exactly what ScriptL lets you do. Since this calls your running lisp, it means you're live coding your commands.
You can get ScriptL from Quicklisp via
(ql:quickload "scriptl"). This will load scriptl and create the
scriptl command for managing scripts (see below).
First however, you must start the server. It may be convenient to create a shell script that launches your favorite CL on login and does the following:
The scripts in
unix-options for argument parsing
as well, so to run them load that system as well:
Now you can call things via the generated shell scripts (you might have
to call them via
./[command] from the local directory):
$ funcall princ '"hello world"' hello world $ eval '(princ "hello world")' hello world $ eval '(+ 1 1)' 2 $
Alternatively, once you have something you want as a more permanent
command, you can use
SCRIPTL:MAKE-COMMAND as above:
(make-command COMMAND-NAME FUNCTION-NAME &optional ERROR-HANDLER)
This will make a shell script called
COMMAND, probably in your home
directory (or wherever you started lisp). It will call
FUNCTION-NAME, with any parameters passed to the script given as
Alternatively, you can use the
make-command script included, which
is a ScriptL wrapper for itself:
$ make-script some-command some-function [error-function]
This will make the new script in the current working directory,
because wrappered calls rebind
*DEFAULT-PATHNAME-DEFAULTS* for your
convenience. Amusingly, you can use
make-script to generate itself:
$ make-script make-script scriptl:make-script scriptl:make-script-usage
This will overwrite itself with an updated copy, assuming you're using an OS which isn't picky about such things.
scriptl and registering scripts
After installing ScriptL, the
scriptl command will be created in the
bin/ directory. This is part of a small API for registering, listing, and creating scripts. For instance:
$ scriptl list Script Description SCRIPTL:SCRIPTL - ScriptL management command: scriptl $ scriptl make SCRIPTL:SCRIPTL $ ls scriptl $
This is more useful with a lot of scripts loaded. For example, after loading my personal scripts:
$ scriptl list Script Description SCRIPTL.SCRIPT.JSON:JSON - JSON manipulation commands: json, json2yaml, sexp2json, dir2json SCRIPTL.SCRIPT.NEW:NEW - Template system command: new SCRIPTL.SCRIPT.YAML:YAML - YAML manipulation commands: yaml2json SCRIPTL:SCRIPTL - ScriptL management command: scriptl $
To register scripts for this list, you should make a function which calls
MAKE-COMMAND for each script you wish to create, and then call
SCRIPTL:REGISTER to register this:
(defun make-my-scripts () (make-command ...) (make-command ...)) (scriptl:register 'my-scripts 'make-my-scripts "My script commands: foo, bar, baz") (export 'my-scripts)
Now you would see the following:
$ scriptl list Script Description MY-PACKAGE:MY-SCRIPTS - My script commands: foo, bar, baz SCRIPTL:SCRIPTL - ScriptL management command: scriptl $
Defaults and the Header
A number of useful things are set up by default:
- The default package is
- Wrappers set the current working directory
- The current script name is set in
- The parsed header is set in
As above, whenever a wrapper is run, ScriptL sets
*DEFAULT-PATHNAME-DEFAULTS* for you, which is enough for most Lisp
functions to find the right file.
However, for things like
osicat, you may need to use
(merge-pathnames FILENAME) to get the appropriate thing.
You may find it interesting to dispatch on the script name, which is
SCRIPTL:*SCRIPT*. It's a pathname, so you can extract the
various bits as appropriate.
Additionally, you can access the complete call information via
(defun show-header (&rest args) scriptl:*header*)
(You don't have to use
&rest args; this is for demonstration.)
$ funcall show-header 1 2 3 #S(SCRIPTL:HEADER :VERSION 1 :CWD #P"/path/to/current/dir/" :COMMAND (:FUNCALL 3 #P"./funcall") :FUN SHOW-HEADER :ARGS ("1" "2" "3") :ERROR-FUN NIL) $
Most of this isn't particularly interesting or beyond what you already get, but it's there.
The v2 client adds simple support for the GNU readline library.
There are two basic operations:
readline PROMPT and
(defun test () (format t "Type 'quit' to finish.~%") (loop as line = (readline "> ") until (string= line "quit") do (addhistory line) (format t "You typed: ~A~%" line)))
In the shell:
Type 'quit' to finish. > foo You typed: foo > quit
If GNU readline support is compiled into the client (autodetected), this will have all the fancy editing features that provides, including history. If not, or if the function is not called from ScriptL, the prompt will be shown, but only basic terminal input is provided, and history is ignored.
Also included in v2 is support for remote
client-side configuration via environment variables. This is done via
This will return the value of a variable on the remote side, or for
functions not being called via a ScriptL command, will fall back to
osicat-posix:getenv and return the environment for the running lisp.
In both cases, this returns the value of the variable as a string if
The v2 protocol adds a custom client which supports standard lisp I/O
(defun test-read-line () (format t "Read line: ~A~%" (read-line)))
$ echo foo | test-read-line Read line: foo $
Additionally, you can use
read-char. The former two return octet vectors, which may be useful
for processing data byte-by-byte.
Reading and writing may be interleaved as desired. Writing also
write-char. In order
to write a raw byte sequence, however, it must be specified as an
:element-type '(unsigned-byte 8). Otherwise it will be
treated as a general object, and be pretty-printed.
Results and Error Handling
When things run correctly, if there is output to
this is shown to the user. If there is no output, the return values
are shown instead:
$ eval '(+ 2 2)' 4 $ eval '(values 1 2 3)' (VALUES 1 2 3) $
When things go wrong, the error condition is shown by default:
$ eval 'foo' Error: UNBOUND-VARIABLE The variable FOO is unbound. $
If you want slightly more sophisticated handling, you can define an
error handler when calling
MAKE-SCRIPT, which will get passed the
HANDLER-BIND. If you handle it, return an non-
value. If you can't handle the condition, return
examples/ directory has a number of examples; most of them are
simply talking to the ScriptL server.
test-cmd example shows how to make a more
shell-scripty-feeling function with more conventional means.
Alternatively, if you look at
src/make-script.lisp, you can see
a (rather naïve) error handler which provides usage.
also handles its parameters appropriately if they're strings.
This all works, of course, by making a server on a port in your Lisp and listening there. I really wanted to use Swank, but in the end the problem went from simply talking to swank to implementing READ in the shell, and that's a lot more work than just writing a new, more targeted server.
For V1, this relied on netcat. V2 presents a custom C client, which builds as part of the ASDF load operation, and supports a number of new features, such as I/O.
ScriptL initially used TCP port 4010. This is bad for a number of reasons .. each user can't have their own lisp, and anyone can access it. V2 uses Unix Domain Sockets by default, and only the owner can access it. This is restricted by file permissions.
You can still use the TCP server, by specifying
(scriptl:start :internet). You can adjust the port by binding
scriptl:*scriptl-port* to the desired port before doing this.
This talks only to localhost. It's horribly insecure, though no
moreso than swank/slime, really. If you're worried, or not the only
person with access to your host, don't run it.
In theory, making this talk over an ssh tunnel would be pretty easy, but switching ports and doing the setup isn't at all nice.