The Common Lisp Cookbook – Scripting. Command line arguments. Executables.

Table of Contents

The Common Lisp Cookbook – Scripting. Command line arguments. Executables.

đź“ą NEW! Learn Lisp in videos and support our contributors with this 40% discount. Recently added: the condition system.

đź“• Get the EPUB and PDF

Using a program from a REPL is fine and well, but if we want to distribute our program easily, we’ll want to build an executable.

Lisp implementations differ in their processes, but they all create self-contained executables, for the architecture they are built on. The final user doesn’t need to install a Lisp implementation, he can run the software right away.

Start-up times are near to zero, specially with SBCL and CCL.

Binaries size are large-ish. They include the whole Lisp including its libraries, the names of all symbols, information about argument lists to functions, the compiler, the debugger, source code location information, and more.

Note that we can similarly build self-contained executables for web apps.

Building a self-contained executable

With SBCL - Images and Executables

How to build (self-contained) executables is, by default, implementation-specific (see below for portable ways). With SBCL, as says its documentation, it is a matter of calling save-lisp-and-die with the :executable argument to T:

(sb-ext:save-lisp-and-die #P"path/name-of-executable" :toplevel #'my-app:main-function :executable t)

sb-ext is an SBCL extension to run external processes. See other SBCL extensions (many of them are made implementation-portable in other libraries).

:executable t tells to build an executable instead of an image. We could build an image to save the state of our current Lisp image, to come back working with it later. This is especially useful if we made a lot of work that is computing intensive. In that case, we re-use the image with sbcl --core name-of-image.

:toplevel gives the program’s entry point, here my-app:main-function. Don’t forget to export the symbol, or use my-app::main-function (with two colons).

If you try to run this in Slime, you’ll get an error about threads running:

Cannot save core with multiple threads running.

We must run the command from a simple SBCL repl, from the terminal.

I suppose your project has Quicklisp dependencies. You must then:

That gives:

(asdf:load-asd "my-app.asd")
(ql:quickload "my-app")
(sb-ext:save-lisp-and-die #p"my-app-binary" :toplevel #'my-app:main :executable t)

From the command line, or from a Makefile, use --load and --eval:

	sbcl --load my-app.asd \
	     --eval '(ql:quickload :my-app)' \
         --eval "(sb-ext:save-lisp-and-die #p\"my-app\" :toplevel #'my-app:main :executable t)"


Now that we’ve seen the basics, we need a portable method. Since its version 3.1, ASDF allows to do that. It introduces the make command, that reads parameters from the .asd. Add this to your .asd declaration:

:build-operation "program-op" ;; leave as is
:build-pathname "<here your final binary name>"
:entry-point "<my-package:main-function>"

and call asdf:make :my-package.

So, in a Makefile:

LISP ?= sbcl

    $(LISP) --load my-app.asd \
    	--eval '(ql:quickload :my-app)' \
		--eval '(asdf:make :my-app)' \
		--eval '(quit)'

With Deploy - ship foreign libraries dependencies

All this is good, you can create binaries that work on your machine… but maybe not on someone else’s or on your server. Your program probably relies on C shared libraries that are defined somewhere on your filesystem. For example, libssl might be located on


but on your VPS, maybe somewhere else.

Deploy to the rescue.

It will create a bin/ directory with your binary and the required foreign libraries. It will auto-discover the ones your program needs, but you can also help it (or tell it to not do so much).

Its use is very close to the above recipe with asdf:make and the .asd project configuration. Use this:

:defsystem-depends-on (:deploy)  ;; (ql:quickload "deploy") before
:build-operation "deploy-op"     ;; instead of "program-op"
:build-pathname "my-application-name"  ;; doesn't change
:entry-point "my-package:my-start-function"  ;; doesn't change

and build your binary with (asdf:make :my-app) like before.

Now, ship the bin/ directory to your users.

When you run the binary, you’ll see it uses the shipped libraries:

$ ./my-app
 ==> Performing warm boot.
   -> Runtime directory is /home/debian/projects/my-app/bin/
   -> Resource directory is /home/debian/projects/my-app/bin/
 ==> Running boot hooks.
 ==> Reloading foreign libraries.
   -> Loading foreign library #<LIBRARY LIBRT>.
   -> Loading foreign library #<LIBRARY LIBMAGIC>.
 ==> Launching application.


A note regarding libssl. It’s easier, on Linux at least, to rely on your OS’ current installation, so we’ll tell Deploy to not bother shipping it (nor libcrypto):

#+linux (deploy:define-library cl+ssl::libssl :dont-deploy T)
#+linux (deploy:define-library cl+ssl::libcrypto :dont-deploy T)

The day you want to ship a foreign library that Deploy doesn’t find, you can instruct it like this:

(deploy:define-library cl+ssl::libcrypto
  ;;                   ^^^ CFFI system name. Find it with a call to "apropos".
  :path "/usr/lib/x86_64-linux-gnu/")

A last remark. Once you built your binary and you run it for the first time, you might get a funny message from ASDF that tries to upgrade itself, finds nothing into a ~/common-lisp/asdf/ repository, and quits. To tell it to not upgrade itself, add this into your .asd:

;; Tell ASDF to not update itself.
(deploy:define-hook (:deploy asdf) (directory)
  (declare (ignorable directory))
  #+asdf (asdf:clear-source-registry)
  #+asdf (defun asdf:upgrade-asdf () nil))

But there is more, so we refer you to Deploy’s documentation.

With Roswell or Buildapp

Roswell, an implementation manager, script launcher and much more, has the ros build command, that should work for many implementations.

This is how we can make our application easily installable by others, with a ros install my-app. See Roswell’s documentation.

Be aware that ros build adds core compression by default. That adds a significant startup overhead of the order of 150ms (for a simple app, startup time went from about 30ms to 180ms). You can disable it with ros build <app.ros> --disable-compression. Of course, core compression reduces your binary size significantly. See the table below, “Size and startup times of executables per implementation”.

We’ll finish with a word on Buildapp, a battle-tested and still popular “application for SBCL or CCL that configures and saves an executable Common Lisp image”.

Example usage:

buildapp --output myapp \
         --asdf-path . \
         --asdf-tree ~/quicklisp/dists \
         --load-system my-app \
         --entry my-app:main

Many applications use it (for example, pgloader), it is available on Debian: apt install buildapp, but you shouldn’t need it now with asdf:make or Roswell.

For web apps

We can similarly build a self-contained executable for our web appplication. It would thus contain a web server and would be able to run on the command line:

$ ./my-web-app
Hunchentoot server is started.
Listening on localhost:9003.

Note that this runs the production webserver, not a development one, so we can run the binary on our VPS right away and access the application from the outside.

We have one thing to take care of, it is to find and put the thread of the running web server on the foreground. In our main function, we can do something like this:

(defun main ()
  (start-app :port 9003) ;; our start-app, for example clack:clack-up
  ;; let the webserver run.
  ;; warning: hardcoded "hunchentoot".
  (handler-case (bt:join-thread (find-if (lambda (th)
                                            (search "hunchentoot" (bt:thread-name th)))
    ;; Catch a user's C-c
    (#+sbcl sb-sys:interactive-interrupt
      #+ccl  ccl:interrupt-signal-condition
      #+clisp system::simple-interrupt-condition
      #+ecl ext:interactive-interrupt
      #+allegro excl:interrupt-signal
      () (progn
           (format *error-output* "Aborting.~&")
           (clack:stop *server*)
    (error (c) (format t "Woops, an unknown error occured:~&~a~&" c))))

We used the bordeaux-threads library ((ql:quickload "bordeaux-threads"), alias bt) and uiop, which is part of ASDF so already loaded, in order to exit in a portable way (uiop:quit, with an optional return code, instead of sb-ext:quit).

Size and startup times of executables per implementation

SBCL isn’t the only Lisp implementation. ECL, Embeddable Common Lisp, transpiles Lisp programs to C. That creates a smaller executable.

According to this reddit source, ECL produces indeed the smallest executables of all, an order of magnitude smaller than SBCL, but with a longer startup time.

CCL’s binaries seem to be as fast to start up as SBCL and nearly half the size.

| program size | implementation |  CPU | startup time |
|           28 | /bin/true      |  15% |        .0004 |
|         1005 | ecl            | 115% |        .5093 |
|        48151 | sbcl           |  91% |        .0064 |
|        27054 | ccl            |  93% |        .0060 |
|        10162 | clisp          |  96% |        .0170 |
|         4901 | ecl.big        | 113% |        .8223 |
|        70413 | sbcl.big       |  93% |        .0073 |
|        41713 | ccl.big        |  95% |        .0094 |
|        19948 | clisp.big      |  97% |        .0259 |

You’ll also want to investigate the proprietary Lisps’ tree shakers capabilities.

Regarding compilation times, CCL is famous for being fast in that regards. ECL is more involved and takes the longer to compile of these three implementations.

Building a smaller binary with SBCL’s core compression

Building with SBCL’s core compression can dramatically reduce your application binary’s size. In our case, it reduced it from 120MB to 23MB, for a loss of a dozen milliseconds of start-up time, which was still under 50ms.

Note: SBCL 2.2.6 switched to compression with zstd instead of zlib, which provides smaller binaries and faster compression and decompression times. Un-official numbers are: about 4x faster compression, 2x faster decompression, and smaller binaries by 10%.

Your SBCL must be built with core compression, see the documentation: Saving-a-Core-Image

Is it the case ?

(find :sb-core-compression *features*)

Yes, it is the case with this SBCL installed from Debian.


In SBCL, we would give an argument to save-lisp-and-die, where :compression

may be an integer from -1 to 9, corresponding to zlib compression levels, or t (which is equivalent to the default compression level, -1).

We experienced a 1MB difference between levels -1 and 9.


However, we prefer to do this with ASDF (or rather, UIOP). Add this in your .asd:

(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
  (uiop:dump-image (asdf:output-file o c) :executable t :compression t))

With Deploy

Also, the Deploy library can be used to build a fully standalone application. It will use compression if available.

Deploy is specifically geared towards applications with foreign library dependencies. It collects all the foreign shared libraries of dependencies, such as in the bin subdirectory.

And voilĂ  !

Parsing command line arguments

SBCL stores the command line arguments into sb-ext:*posix-argv*.

But that variable name differs from implementations, so we want a way to handle the differences for us.

We have uiop:command-line-arguments, shipped in ASDF and included in nearly all implementations. From anywhere in your code, you can simply check if a given string is present in this list:

(member "-h" (uiop:command-line-arguments) :test #'string-equal)

That’s good, but we also want to parse the arguments, have facilities to check short and long options, build a help message automatically, etc.

A quick look at the awesome-cl#scripting list made us choose the unix-opts library.

(ql:quickload "unix-opts")

We can call it with its opts alias (a global nickname).

As often work happens in two phases:

Declaring arguments

We define the arguments with opts:define-opts:

    (:name :help
           :description "print this help text"
           :short #\h
           :long "help")
    (:name :nb
           :description "here we want a number argument"
           :short #\n
           :long "nb"
           :arg-parser #'parse-integer) ;; <- takes an argument
    (:name :info
           :description "info"
           :short #\i
           :long "info"))

Here parse-integer is a built-in CL function. If the argument you expect is a string, you don’t have to define an arg-parser.

Here is an example output on the command line after we build and run a binary of our application. The help message was auto-generated:

$ my-app -h
my-app. Usage:

Available options:
  -h, --help               print this help text
  -n, --nb ARG             here we want a number argument
  -i, --info               info


We parse and get the arguments with opts:get-opts, which returns two values: the list of valid options and the remaining free arguments. We then must use multiple-value-bind to assign both into variables:

  (multiple-value-bind (options free-args)
      ;; There is no error handling yet.

We can test this by giving a list of strings to get-opts:

(multiple-value-bind (options free-args)
                   (opts:get-opts '("hello" "-h" "-n" "1"))
                 (format t "Options: ~a~&" options)
                 (format t "free args: ~a~&" free-args))
Options: (HELP T NB-RESULTS 1)
free args: (hello)

If we put an unknown option, we get into the debugger. We’ll see error handling in a moment.

So options is a property list. We use getf and setf with plists, so that’s how we do our logic. Below we print the help with opts:describe and then we quit (in a portable way).

  (multiple-value-bind (options free-args)

    (if (getf options :help)
           :prefix "You're in my-app. Usage:"
           :args "[keywords]") ;; to replace "ARG" in "--nb ARG"
    (if (getf options :nb)

For a full example, see its official example and cl-torrents’ tutorial.

The example in the unix-opts repository suggests a macro to do slightly better. Now to error handling.

Handling malformed or missing arguments

There are 4 situations that unix-opts doesn’t handle, but signals conditions for us to take care of:

So, we must create simple functions to handle those conditions, and surround the parsing of the options with an handler-bind form:

  (multiple-value-bind (options free-args)
      (handler-bind ((opts:unknown-option #'unknown-option) ;; the condition / our function
                     (opts:missing-arg #'missing-arg)
                     (opts:arg-parser-failed #'arg-parser-failed)
    ;; use "options" and "free-args"

Here we suppose we want one function to handle each case, but it could be a simple one. They take the condition as argument.

(defun handle-arg-parser-condition (condition)
  (format t "Problem while parsing option ~s: ~a .~%" (opts:option condition) ;; reader to get the option from the condition.
  (opts:describe) ;; print help
  (uiop:quit 1))

For more about condition handling, see error and condition handling.

Catching a C-c termination signal

Let’s build a simple binary, run it, try a C-c and read the stacktrace:

$ ./my-app
debugger invoked on a SB-SYS:INTERACTIVE-INTERRUPT in thread   <== condition name
#<THREAD "main thread" RUNNING {1003156A03}>:
  Interactive interrupt at #x7FFFF6C6C170.

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [CONTINUE     ] Return from SB-UNIX:SIGINT.               <== it was a SIGINT indeed
  1: [RETRY-REQUEST] Retry the same request.

The signaled condition is named after our implementation: sb-sys:interactive-interrupt. We just have to surround our application code with a handler-case:

    (run-my-app free-args)
  (sb-sys:interactive-interrupt () (progn
                                     (format *error-output* "Abort.~&")

This code is only for SBCL though. We know about trivial-signal, but we were not satisfied with our test yet. So we can use something like this:

    (run-my-app free-args)
  (#+sbcl sb-sys:interactive-interrupt
   #+ccl  ccl:interrupt-signal-condition
   #+clisp system::simple-interrupt-condition
   #+ecl ext:interactive-interrupt
   #+allegro excl:interrupt-signal

here #+ includes the line at compile time depending on the implementation. There’s also #-. What #+ does is to look for symbols in the *features* list. We can also combine symbols with and, or and not.

Continuous delivery of executables

We can make a Continuous Integration system (Travis CI, Gitlab CI,…) build binaries for us at every commit, or at every tag pushed or at whichever other policy.

See Continuous Integration.


Page source:

© 2002–2021 the Common Lisp Cookbook Project