1
0
Fork 0
cl-sites/lispcookbook.github.io/cl-cookbook/scripting.html

925 lines
37 KiB
HTML
Raw Normal View History

2023-10-25 11:23:21 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Linux version 5.2.0">
<title>Scripting. Command line arguments. Executables.</title>
<meta charset="utf-8">
<meta name="description" content="A collection of examples of using Common Lisp">
<meta name="viewport" content=
"width=device-width, initial-scale=1">
<link rel="icon" href=
"assets/cl-logo-blue.png"/>
<link rel="stylesheet" href=
"assets/style.css">
<script type="text/javascript" src=
"assets/highlight-lisp.js">
</script>
<script type="text/javascript" src=
"assets/jquery-3.2.1.min.js">
</script>
<script type="text/javascript" src=
"assets/jquery.toc/jquery.toc.min.js">
</script>
<script type="text/javascript" src=
"assets/toggle-toc.js">
</script>
<link rel="stylesheet" href=
"assets/github.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<h1 id="title-xs"><a href="index.html">The Common Lisp Cookbook</a> &ndash; Scripting. Command line arguments. Executables.</h1>
<div id="logo-container">
<a href="index.html">
<img id="logo" src="assets/cl-logo-blue.png"/>
</a>
<div id="searchform-container">
<form onsubmit="duckSearch()" action="javascript:void(0)">
<input id="searchField" type="text" value="" placeholder="Search...">
</form>
</div>
<div id="toc-container" class="toc-close">
<div id="toc-title">Table of Contents</div>
<ul id="toc" class="list-unstyled"></ul>
</div>
</div>
<div id="content-container">
<h1 id="title-non-xs"><a href="index.html">The Common Lisp Cookbook</a> &ndash; Scripting. Command line arguments. Executables.</h1>
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
2024-05-15 18:18:38 +02:00
<!-- <p class="announce"> -->
<!-- 📢 🤶 ⭐ -->
<!-- <a style="font-size: 120%" href="https://www.udemy.com/course/common-lisp-programming/?couponCode=LISPY-XMAS2023" title="This course is under a paywall on the Udemy platform. Several videos are freely available so you can judge before diving in. vindarel is (I am) the main contributor to this Cookbook."> Discover our contributor's Lisp course with this Christmas coupon.</a> -->
<!-- <strong> -->
<!-- Recently added: 18 videos on MACROS. -->
<!-- </strong> -->
<!-- <a style="font-size: 90%" href="https://github.com/vindarel/common-lisp-course-in-videos/">Learn more</a>. -->
<!-- </p> -->
<p class="announce">
📢 New videos: <a href="https://www.youtube.com/watch?v=h_noB1sI_e8">web dev demo part 1</a>, <a href="https://www.youtube.com/watch?v=xnwc7irnc8k">dynamic page with HTMX</a>, <a href="https://www.youtube.com/watch?v=Zpn86AQRVN8">Weblocks demo</a>
</p>
2023-10-25 11:23:21 +02:00
<p class="announce-neutral">
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
</p>
<div id="content"
2024-01-12 09:23:31 +01:00
<p>Using a program from a REPL is fine and well, but once its ready
well surely want to call it from the terminal. We can run Lisp
<strong>scripts</strong> for this.</p>
<p>Next, if we want to distribute our program easily, well want to build
an <strong>executable</strong>.</p>
2023-10-25 11:23:21 +02:00
<p>Lisp implementations differ in their processes, but they all create
<strong>self-contained executables</strong>, for the architecture they are built on. The
final user doesnt need to install a Lisp implementation, he can run
the software right away.</p>
<p><strong>Start-up times</strong> are near to zero, specially with SBCL and CCL.</p>
<p>Binaries <strong>size</strong> 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.</p>
<p>Note that we can similarly build self-contained executables for <strong>web apps</strong>.</p>
2024-01-12 09:23:31 +01:00
<h2 id="scripting-with-common-lisp">Scripting with Common Lisp</h2>
<p>Create a file named <code>hello</code> (you can drop the .lisp extension) and add this:</p>
<pre><code>#!/usr/bin/env -S sbcl --script
(require :uiop)
(format t "hello ~a!~&amp;" (uiop:getenv "USER"))
</code></pre>
<p>Make the script executable (<code>chmod +x hello</code>) and run it:</p>
<pre><code>$ ./hello
hello me!
</code></pre>
<p>Nice! We can use this to a great extent already.</p>
<p>In addition, the script was quite fast to start, 0.03s on my system.</p>
<p>However, we will get longer startup times as soon as we add
dependencies. The solution is to build a binary. They start even
faster, with all dependencies compiled.</p>
<h3 id="quickloading-dependencies-from-a-script">Quickloading dependencies from a script</h3>
<p>Say you dont bother with an .asd project definition yet, you just
want to write a quick script, but you need to load a quicklisp
dependency. Youll need a bit more ceremony:</p>
<pre><code class="language-lisp">#!/usr/bin/env -S sbcl --script
(require :uiop)
;; We want quicklisp, which is loaded from our initfile,
;; after a classical installation.
;; However the --script flag doesn't load our init file:
;; it implies --no-sysinit --no-userinit --disable-debugger --end-toplevel-options
;; So, please load it:
(load "~/.sbclrc")
;; Load a quicklisp dependency silently.
(ql:quickload "str" :silent t)
(princ (str:concat "hello " (uiop:getenv "USER") "!"))
</code></pre>
<p>Accordingly, you could only use <code>require</code>, if the quicklisp dependency is already installed:</p>
<pre><code class="language-lisp">;; replace loading sbclrc and ql:quickload.
(require :str)
</code></pre>
<p>Also note that when you put a <code>ql:quickload</code> in the middle of your
code, you cant load the file anymore, you cant <code>C-c C-k</code> from your
editor. This is because the reader will see the “quickload” without
running it yet, then sees “str:concat”, a call to a package that
doesnt exist (it wasnt loaded yet). Common Lisp has you covered,
with a form that executes code during the read phase:</p>
<pre><code class="language-lisp">;; you shouldn't need this. Use an .asd system definition!
(eval-when (:load-toplevel :compile-toplevel :execute)
(ql:quickload "str" :silent t))
</code></pre>
<p>but ASDF project definitions are here for a reason. Find me another
language that makes you install dependencies in the middle of the
application code.</p>
2023-10-25 11:23:21 +02:00
<h2 id="building-a-self-contained-executable">Building a self-contained executable</h2>
<h3 id="with-sbcl---images-and-executables">With SBCL - Images and Executables</h3>
<p>How to build (self-contained) executables is, by default, implementation-specific (see
below for portable ways). With SBCL, as says
<a href="http://www.sbcl.org/manual/index.html#Function-sb_002dext_003asave_002dlisp_002dand_002ddie">its documentation</a>,
it is a matter of calling <code>save-lisp-and-die</code> with the <code>:executable</code> argument to T:</p>
2024-01-12 09:23:31 +01:00
<pre><code class="language-lisp">(sb-ext:save-lisp-and-die #P"path/name-of-executable"
:toplevel #'my-app:main-function
:executable t)
2023-10-25 11:23:21 +02:00
</code></pre>
<p><code>sb-ext</code> is an SBCL extension to run external processes. See other
<a href="http://www.sbcl.org/manual/index.html#Extensions">SBCL extensions</a>
(many of them are made implementation-portable in other libraries).</p>
<p><code>:executable t</code> 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 <code>sbcl --core name-of-image</code>.</p>
<p><code>:toplevel</code> gives the programs entry point, here <code>my-app:main-function</code>. Dont forget to <code>export</code> the symbol, or use <code>my-app::main-function</code> (with two colons).</p>
<p>If you try to run this in Slime, youll get an error about threads running:</p>
<blockquote>
<p>Cannot save core with multiple threads running.</p>
</blockquote>
<p>We must run the command from a simple SBCL repl, from the terminal.</p>
<p>I suppose your project has Quicklisp dependencies. You must then:</p>
<ul>
<li>ensure Quicklisp is installed and loaded at the Lisp startup (you
completed Quicklisp installation),</li>
<li><code>asdf:load-asd</code> the projects .asd (recommended instead of just <code>load</code>),</li>
<li>install the dependencies,</li>
<li>build the executable.</li>
</ul>
<p>That gives:</p>
<pre><code class="language-lisp">(asdf:load-asd "my-app.asd")
(ql:quickload "my-app")
2024-01-12 09:23:31 +01:00
(sb-ext:save-lisp-and-die #p"my-app-binary"
:toplevel #'my-app:main
:executable t)
2023-10-25 11:23:21 +02:00
</code></pre>
<p>From the command line, or from a Makefile, use <code>--load</code> and <code>--eval</code>:</p>
<pre><code>build:
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)"
</code></pre>
<h3 id="with-asdf">With ASDF</h3>
<p>Now that weve seen the basics, we need a portable method. Since its
version 3.1, ASDF allows to do that. It introduces the <a href="https://common-lisp.net/project/asdf/asdf.html#Convenience-Functions"><code>make</code> command</a>,
that reads parameters from the .asd. Add this to your .asd declaration:</p>
<pre><code>:build-operation "program-op" ;; leave as is
:build-pathname "&lt;here your final binary name&gt;"
:entry-point "&lt;my-package:main-function&gt;"
</code></pre>
<p>and call <code>asdf:make :my-package</code>.</p>
<p>So, in a Makefile:</p>
<pre><code class="language-lisp">LISP ?= sbcl
build:
$(LISP) --load my-app.asd \
--eval '(ql:quickload :my-app)' \
--eval '(asdf:make :my-app)' \
--eval '(quit)'
</code></pre>
<h3 id="with-deploy---ship-foreign-libraries-dependencies">With Deploy - ship foreign libraries dependencies</h3>
<p>All this is good, you can create binaries that work on your machine…
but maybe not on someone elses or on your server. Your program
probably relies on C shared libraries that are defined somewhere on
your filesystem. For example, <code>libssl</code> might be located on</p>
<pre><code>/usr/lib/x86_64-linux-gnu/libssl.so.1.1
</code></pre>
<p>but on your VPS, maybe somewhere else.</p>
<p><a href="https://github.com/Shinmera/deploy">Deploy</a> to the rescue.</p>
<p>It will create a <code>bin/</code> 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).</p>
<p>Its use is very close to the above recipe with <code>asdf:make</code> and the
<code>.asd</code> project configuration. Use this:</p>
<pre><code class="language-lisp">: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
</code></pre>
<p>and build your binary with <code>(asdf:make :my-app)</code> like before.</p>
<p>Now, ship the <code>bin/</code> directory to your users.</p>
<p>When you run the binary, youll see it uses the shipped libraries:</p>
<pre><code class="language-lisp">$ ./my-app
==&gt; Performing warm boot.
-&gt; Runtime directory is /home/debian/projects/my-app/bin/
-&gt; Resource directory is /home/debian/projects/my-app/bin/
==&gt; Running boot hooks.
==&gt; Reloading foreign libraries.
-&gt; Loading foreign library #&lt;LIBRARY LIBRT&gt;.
-&gt; Loading foreign library #&lt;LIBRARY LIBMAGIC&gt;.
==&gt; Launching application.
[…]
</code></pre>
<p>Success!</p>
<p>A note regarding <code>libssl</code>. Its easier, on Linux at least, to
rely on your OS current installation, so well tell Deploy to not
bother shipping it (nor <code>libcrypto</code>):</p>
<pre><code class="language-lisp">#+linux (deploy:define-library cl+ssl::libssl :dont-deploy T)
#+linux (deploy:define-library cl+ssl::libcrypto :dont-deploy T)
</code></pre>
<p>The day you want to ship a foreign library that Deploy doesnt find, you can instruct it like this:</p>
<pre><code class="language-lisp">(deploy:define-library cl+ssl::libcrypto
2024-01-12 09:23:31 +01:00
;; ^^^ CFFI system name.
;; Find it with a call to "apropos".
2023-10-25 11:23:21 +02:00
:path "/usr/lib/x86_64-linux-gnu/libcrypto.so.1.1")
</code></pre>
<p>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 <code>~/common-lisp/asdf/</code> repository, and
quits. To tell it to not upgrade itself, add this into your .asd:</p>
<pre><code class="language-lisp">;; 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))
</code></pre>
<p>You can also silence Deploys start-up messages by adding this in your build script, before <code>asdf:make</code> is called:</p>
<pre><code>(push :deploy-console *features*)
</code></pre>
<p>And there is more, so we refer you to Deploys documentation.</p>
<h3 id="with-roswell-or-buildapp">With Roswell or Buildapp</h3>
<p><a href="https://roswell.github.io">Roswell</a>, an implementation manager, script launcher and
much more, has the <code>ros build</code> command, that should work for many
implementations.</p>
<p>This is how we can make our application easily installable by others, with a <code>ros install
my-app</code>. See Roswells documentation.</p>
<p>Be aware that <code>ros build</code> 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 <code>ros build &lt;app.ros&gt; --disable-compression</code>. Of course, core
compression reduces your binary size significantly. See the table
below, “Size and startup times of executables per implementation”.</p>
<p>Well finish with a word on
<a href="http://www.xach.com/lisp/buildapp/">Buildapp</a>, a battle-tested and
still popular “application for SBCL or CCL that configures and saves
an executable Common Lisp image”.</p>
<p>Example usage:</p>
<pre><code class="language-lisp">buildapp --output myapp \
--asdf-path . \
--asdf-tree ~/quicklisp/dists \
--load-system my-app \
--entry my-app:main
</code></pre>
<p>Many applications use it (for example,
<a href="https://github.com/dimitri/pgloader">pgloader</a>), it is available on
Debian: <code>apt install buildapp</code>, but you shouldnt need it now with asdf:make or Roswell.</p>
<h3 id="for-web-apps">For web apps</h3>
<p>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:</p>
<pre><code>$ ./my-web-app
Hunchentoot server is started.
Listening on localhost:9003.
</code></pre>
<p>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.</p>
<p>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 <code>main</code> function, we
can do something like this:</p>
<pre><code class="language-lisp">(defun main ()
(start-app :port 9003) ;; our start-app, for example clack:clack-up
;; let the webserver run.
;; warning: hardcoded "hunchentoot".
2024-01-12 09:23:31 +01:00
;; You can simply run (sleep most-positive-fixnum)
2023-10-25 11:23:21 +02:00
(handler-case (bt:join-thread (find-if (lambda (th)
(search "hunchentoot" (bt:thread-name th)))
(bt:all-threads)))
;; 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.~&amp;")
(clack:stop *server*)
(uiop:quit)))
(error (c) (format t "Woops, an unknown error occured:~&amp;~a~&amp;" c))))
</code></pre>
<p>We used the <code>bordeaux-threads</code> library (<code>(ql:quickload
"bordeaux-threads")</code>, alias <code>bt</code>) and <code>uiop</code>, which is part of ASDF so
already loaded, in order to exit in a portable way (<code>uiop:quit</code>, with
an optional return code, instead of <code>sb-ext:quit</code>).</p>
<h3 id="size-and-startup-times-of-executables-per-implementation">Size and startup times of executables per implementation</h3>
2024-01-12 09:23:31 +01:00
<p><strong>SBCL</strong> isnt the only Lisp implementation.
<a href="https://gitlab.com/embeddable-common-lisp/ecl/"><strong>ECL</strong></a>, Embeddable
2023-10-25 11:23:21 +02:00
Common Lisp, transpiles Lisp programs to C. That creates a smaller
executable.</p>
<p>According to
<a href="https://www.reddit.com/r/lisp/comments/46k530/tackling_the_eternal_problem_of_lisp_image_size/">this reddit source</a>, ECL produces indeed the smallest executables of all,
an order of magnitude smaller than SBCL, but with a longer startup time.</p>
<p>CCLs binaries seem to be as fast to start up as SBCL and nearly half the size.</p>
<pre><code>| 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 |
</code></pre>
2024-01-12 09:23:31 +01:00
<!-- TODO: what about SBCL with maximum core compression? -->
2023-10-25 11:23:21 +02:00
2024-01-12 09:23:31 +01:00
<p>Regarding compilation times, <strong>CCL</strong> is famous for being fast in that regards.
2023-10-25 11:23:21 +02:00
ECL is more involved and takes the longer to compile of these three implementations.</p>
2024-01-12 09:23:31 +01:00
<p>Youll also want to investigate the proprietary Lisps <strong>tree shakers</strong> capabilities.
<strong>LispWorks</strong> can build a 8MB hello-world program, without compression but fully tree-shaken.
Such an executable is generated in about 1 second and the runtime is inferior to 0.02 seconds on an Apple M2 Pro CPU.</p>
2023-10-25 11:23:21 +02:00
<h3 id="building-a-smaller-binary-with-sbcls-core-compression">Building a smaller binary with SBCLs core compression</h3>
<p>Building with SBCLs core compression can dramatically reduce your
application binarys 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.</p>
<div class="info-box info">
<strong>Note:</strong> 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%.
</div>
<p>Your SBCL must be built with core compression, see the documentation: <a href="http://www.sbcl.org/manual/#Saving-a-Core-Image">Saving-a-Core-Image</a></p>
<p>Is it the case ?</p>
<pre><code class="language-lisp">(find :sb-core-compression *features*)
:SB-CORE-COMPRESSION
</code></pre>
<p>Yes, it is the case with this SBCL installed from Debian.</p>
<p><strong>With SBCL</strong></p>
<p>In SBCL, we would give an argument to <code>save-lisp-and-die</code>, where
<code>:compression</code></p>
<blockquote>
<p>may be an integer from -7 to 22, corresponding to zstd compression levels, or t (which is equivalent to the default compression level, 9).</p>
</blockquote>
<p>For a simple “Hello, world” program:</p>
<pre><code>| Program size | Compression level |
|--------------|---------------------|
| 46MB | Without compression |
| 22MB | -7 |
| 12MB | 9 |
| 11MB | 22 |
</code></pre>
<p>For a bigger project like StumpWM, an X window manager written in Lisp:</p>
<pre><code>| Program size | Compression level |
|--------------|---------------------|
| 58MB | Without compression |
| 27MB | -7 |
| 15MB | 9 |
| 13MB | 22 |
</code></pre>
<p><strong>With ASDF</strong></p>
<p>However, we prefer to do this with ASDF (or rather, UIOP). Add this in your .asd:</p>
<pre><code class="language-lisp">#+sb-core-compression
(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
2024-01-12 09:23:31 +01:00
(uiop:dump-image (asdf:output-file o c)
:executable t
:compression t))
2023-10-25 11:23:21 +02:00
</code></pre>
<p><strong>With Deploy</strong></p>
<p>Also, the <a href="https://github.com/Shinmera/deploy/">Deploy</a> library can be used
to build a fully standalone application. It will use compression if available.</p>
<p>Deploy is specifically geared towards applications with foreign
library dependencies. It collects all the foreign shared libraries of
dependencies, such as libssl.so in the <code>bin</code> subdirectory.</p>
<p>And voilà !</p>
<h2 id="parsing-command-line-arguments">Parsing command line arguments</h2>
<p>SBCL stores the command line arguments into <code>sb-ext:*posix-argv*</code>.</p>
<p>But that variable name differs from implementations, so we want a
way to handle the differences for us.</p>
<p>We have <code>(uiop:command-line-arguments)</code>, 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:</p>
<pre><code class="language-lisp">(member "-h" (uiop:command-line-arguments) :test #'string-equal)
</code></pre>
<p>Thats good, but we also want to parse the arguments, have facilities to check short and long options, build a help message automatically, etc.</p>
<p>We chose the <a href="https://github.com/dnaeon/clingon">Clingon</a> library,
because it may have the richest feature set:</p>
<ul>
<li>it handles subcommands,</li>
<li>it supports various kinds of options (flags, integers, booleans, counters, enums…),</li>
<li>it generates Bash and Zsh completion files as well as man pages,</li>
<li>it is extensible in many ways,</li>
<li>we can easily try it out on the REPL</li>
<li>etc</li>
</ul>
<p>Lets download it:</p>
<pre><code>(ql:quickload "clingon")
</code></pre>
<p>As often, work happens in two phases:</p>
<ul>
<li>we first declare the options that our application accepts, their
kind (flag, string, integer…), their long and short names and the
required ones.</li>
<li>we ask Clingon to parse the command-line options and run our app.</li>
</ul>
<h3 id="declaring-options">Declaring options</h3>
<p>We want to represent a command-line tool with this possible usage:</p>
<pre><code>$ myscript [-h, --help] [-n, --name NAME]
</code></pre>
<p>Ultimately, we need to create a Clingon command (with
<code>clingon:make-command</code>) to represent our application. A command is
composed of options and of a handler function, to do the logic.</p>
<p>So first, lets create options. Clingon already handles “help” for us, but not the short version. Heres how we use <code>clingon:make-option</code> to create an option:</p>
<pre><code class="language-lisp">(clingon:make-option
:flag ;; &lt;--- option kind. A "flag" does not expect a parameter on the CLI.
:description "short help"
;; :long-name "help" ;; &lt;--- long name, sans the "--" prefix, but here it's a duplicate.
:short-name #\h ;; &lt;--- short name, a character
;; :required t ;; &lt;--- is this option always required? In our case, no.
:key :help) ;; &lt;--- the internal reference to use with getopt, see later.
</code></pre>
<p>This is a <strong>flag</strong>: if “-h” is present on the command-line, the
options value will be truthy, otherwise it will be falsy. A flag does
not expect an argument, its here for itself.</p>
<p>Similar kind of options would be:</p>
<ul>
<li><code>:boolean</code>: that one expects an argument, which can be “true” or 1 to be truthy. Anything else is considered falsy.</li>
<li><code>:counter</code>: a counter option counts how many times the option is provided on the command line. Typically, use it with <code>-v</code> / <code>--verbose</code>, so the user could use <code>-vvv</code> to have extra verbosity. In that case, the option value would be 3. When this option is not provided on the command line, Clingon sets its value to 0.</li>
</ul>
<p>Well create a second option (“name” or “-n” with a parameter) and we put everything in a litle function.</p>
<pre><code class="language-lisp">;; The naming with a "/" is just our convention.
(defun cli/options ()
"Returns a list of options for our main command"
(list
(clingon:make-option
:flag
:description "short help."
:short-name #\h
:key :help)
(clingon:make-option
:string ;; &lt;--- string type: expects one parameter on the CLI.
:description "Name to greet"
:short-name #\n
:long-name "name"
:env-vars '("USER") ;; &lt;-- takes this default value if the env var exists.
:initial-value "lisper" ;; &lt;-- default value if nothing else is set.
:key :name)))
</code></pre>
<p>The second option we created is of kind <code>:string</code>. This option expects one argument, which will be parsed as a string. There is also <code>:integer</code>, to parse the argument as an integer.</p>
<p>There are more option kinds of Clingon, which you will find on its good documentation: <code>:choice</code>, <code>:enum</code>, <code>:list</code>, <code>:filepath</code>, <code>:switch</code> and so on.</p>
<h3 id="top-level-command">Top-level command</h3>
<p>We have to tell Clingon about our top-level command.
<code>clingon:make-command</code> accepts some descriptive fields, and two important ones:</p>
<ul>
<li><code>:options</code> is a list of Clingon options, each created with <code>clingon:make-option</code></li>
<li><code>:handler</code> is the function that will do the apps logic.</li>
</ul>
<p>And finally, well use <code>clingon:run</code> in our main function (the entry
point of our binary) to parse the command-line arguments, and apply
our commands logic. During development, we can also manually call
<code>clingon:parse-command-line</code> to try things out.</p>
<p>Heres a minimal command. Well define our handler function afterwards:</p>
<pre><code class="language-lisp">(defun cli/command ()
"A command to say hello to someone"
(clingon:make-command
:name "hello"
:description "say hello"
:version "0.1.0"
:authors '("John Doe &lt;john.doe@example.org")
:license "BSD 2-Clause"
:options (cli/options) ;; &lt;-- our options
:handler #'null)) ;; &lt;-- to change. See below.
</code></pre>
<p>At this point, we can already test things out on the REPL.</p>
<h3 id="testing-options-parsing-on-the-repl">Testing options parsing on the REPL</h3>
<p>Use <code>clingon:parse-command-line</code>: it wants a top-level command, and a list of command-line arguments (strings):</p>
<pre><code class="language-lisp">CL-USER&gt; (clingon:parse-command-line (cli/command) '("-h" "-n" "me"))
#&lt;CLINGON.COMMAND:COMMAND name=hello options=5 sub-commands=0&gt;
</code></pre>
<p>It works!</p>
<p>We can even <code>inspect</code> this command object, we would see its properties (name, hooks, description, context…), its list of options, etc.</p>
<p>Lets try again with an unknown option:</p>
<pre><code class="language-lisp">CL-USER&gt; (clingon:parse-command-line (cli/command) '("-x"))
;; =&gt; debugger: Unknown option -x of kind SHORT
</code></pre>
<p>In that case, we are dropped into the interactive debugger, which says</p>
<pre><code>Unknown option -x of kind SHORT
[Condition of type CLINGON.CONDITIONS:UNKNOWN-OPTION]
</code></pre>
<p>and we are provided a few restarts:</p>
<pre><code>Restarts:
0: [DISCARD-OPTION] Discard the unknown option
1: [TREAT-AS-ARGUMENT] Treat the unknown option as a free argument
2: [SUPPLY-NEW-VALUE] Supply a new value to be parsed
3: [RETRY] Retry SLIME REPL evaluation request.
4: [*ABORT] Return to SLIME's top level.
</code></pre>
<p>which are very practical. If we needed, we could create an <code>:around</code>
method for <code>parse-command-line</code>, handle Clingons conditions with
<code>handler-bind</code> and use its restarts, to do something different with
unknown options. But we dont need that yet, if ever: we want our
command-line parsing engine to warn us on invalid options.</p>
<p>Last but not least, we can see how Clingon prints our CLI tools usage information:</p>
<pre><code>CL-USER&gt; (clingon:print-usage (cli/command) t)
NAME:
hello - say hello
USAGE:
hello [options] [arguments ...]
OPTIONS:
--help display usage information and exit
--version display version and exit
-h short help.
-n, --name &lt;VALUE&gt; Name to greet [default: lisper] [env: $USER]
AUTHORS:
John Doe &lt;john.doe@example.org
LICENSE:
BSD 2-Clause
</code></pre>
<p>We can tweak the “USAGE” part with the <code>:usage</code> key parameter of the lop-level command.</p>
<h3 id="handling-options">Handling options</h3>
<p>When the parsing of command-line arguments succeeds, we need to do something with them. We introduce two new Clingon functions:</p>
<ul>
<li><code>clingon:getopt</code> is used to get an options value by its <code>:key</code></li>
<li><code>clingon:command-arguments</code> gets use the free arguments remaining on the command-line.</li>
</ul>
<p>Heres how to use them:</p>
<pre><code class="language-lisp">CL-USER&gt; (let ((command (clingon:parse-command-line (cli/command) '("-n" "you" "last"))))
(format t "name is: ~a~&amp;" (clingon:getopt command :name))
(format t "free args are: ~s~&amp;" (clingon:command-arguments command)))
name is: you
free args are: ("last")
NIL
</code></pre>
<p>It is with them that we will write the handler of our top-level command:</p>
<pre><code class="language-lisp">(defun cli/handler (cmd)
"The handler function of our top-level command"
(let ((free-args (clingon:command-arguments cmd))
(name (clingon:getopt cmd :name))) ;; &lt;-- using the option's :key
(format t "Hello, ~a!~%" name)
2024-01-12 09:23:31 +01:00
(format t "You have provided ~a more free arguments~%"
(length free-args))
2023-10-25 11:23:21 +02:00
(format t "Bye!~%")))
</code></pre>
<p>We must tell our top-level command to use this handler:</p>
<pre><code class="language-lisp">;; from above:
(defun cli/command ()
"A command to say hello to someone"
(clingon:make-command
...
:handler #'cli/handler)) ;; &lt;-- changed.
</code></pre>
<p>We now only have to write the main entry point of our binary and were done.</p>
<p>By the way, <code>clingon:getopt</code> returns 3 values:</p>
<ul>
<li>the options value</li>
<li>a boolean, indicating wether this option was provided on the command-line</li>
<li>the command which provided the option for this value.</li>
</ul>
<p>See also <code>clingon:opt-is-set-p</code>.</p>
<h3 id="main-entry-point">Main entry point</h3>
<p>This can be any function, but to use Clingon, use its <code>run</code> function:</p>
<pre><code class="language-lisp">(defun main ()
"The main entrypoint of our CLI program"
(clingon:run (cli/command)))
</code></pre>
<p>To use this main function as your binary entry point, see above how to build a Common Lisp binary. A reminder: set it in your .asd system declaration:</p>
<pre><code class="language-lisp">:entry-point "my-package::main"
</code></pre>
<p>And thats about it. Congratulations, you can now properly parse command-line arguments!</p>
<p>Go check Clingons documentation, because there is much more to it: sub-commands, contexts, hooks, handling a C-c, developing new options such as an email kind, Bash and Zsh completion…</p>
<h2 id="catching-a-c-c-termination-signal">Catching a C-c termination signal</h2>
<p>By default, <strong>Clingon provides a handler for SIGINT signals</strong>. It makes the
application to immediately exit with the status code 130.</p>
<p>If your application needs some clean-up logic, you can use an <code>unwind-protect</code> form. However, it might not be appropriate for all cases, so Clingon advertises to use the <a href="https://github.com/compufox/with-user-abort">with-user-abort</a> helper library.</p>
<p>Below we show how to catch a C-c manually. Because by default, you would get a Lisp stacktrace.</p>
<p>We built a simple binary, we ran it and pressed <code>C-c</code>. Lets read the stacktrace:</p>
<pre><code>$ ./my-app
sleep…
^C
debugger invoked on a SB-SYS:INTERACTIVE-INTERRUPT in thread &lt;== condition name
#&lt;THREAD "main thread" RUNNING {1003156A03}&gt;:
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. &lt;== it was a SIGINT indeed
1: [RETRY-REQUEST] Retry the same request.
</code></pre>
<p>The signaled condition is named after our implementation:
<code>sb-sys:interactive-interrupt</code>. We just have to surround our
application code with a <code>handler-case</code>:</p>
<pre><code class="language-lisp">(handler-case
(run-my-app free-args)
2024-01-12 09:23:31 +01:00
(sb-sys:interactive-interrupt ()
(progn
(format *error-output* "Abort.~&amp;")
(opts:exit))))
2023-10-25 11:23:21 +02:00
</code></pre>
<p>This code is only for SBCL though. We know about
<a href="https://github.com/guicho271828/trivial-signal/">trivial-signal</a>,
but we were not satisfied with our test yet. So we can use something
like this:</p>
<pre><code class="language-lisp">(handler-case
(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
()
(opts:exit)))
</code></pre>
<p>here <code>#+</code> includes the line at compile time depending on
the implementation. Theres also <code>#-</code>. What <code>#+</code> does is to look for
symbols in the <code>*features*</code> list. We can also combine symbols with
<code>and</code>, <code>or</code> and <code>not</code>.</p>
<h2 id="continuous-delivery-of-executables">Continuous delivery of executables</h2>
<p>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.</p>
<p>See <a href="testing.html#continuous-integration">Continuous Integration</a>.</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="https://github.com/sionescu/sbcl-goodies">SBCL-GOODIES</a> - Allows to distribute SBCL binaries with foreign libraries: <code>libssl</code>, <code>libcrypto</code> and <code>libfixposix</code> are statically baked in. This removes the need of Deploy, when only these three foreign libraries are used.
<ul>
<li>it was released on February, 2023.</li>
</ul>
</li>
</ul>
<h2 id="credit">Credit</h2>
<ul>
<li><a href="https://vindarel.github.io/cl-torrents/tutorial.html">cl-torrents tutorial</a></li>
<li><a href="https://lisp-journey.gitlab.io/web-dev/">lisp-journey/web-dev</a></li>
</ul>
<p class="page-source">
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/scripting.md">scripting.md</a>
</p>
</div>
<script type="text/javascript">
// Don't write the TOC on the index.
if (window.location.pathname != "/cl-cookbook/") {
$("#toc").toc({
content: "#content", // will ignore the first h1 with the site+page title.
headings: "h1,h2,h3,h4"});
}
$("#two-cols + ul").css({
"column-count": "2",
});
$("#contributors + ul").css({
"column-count": "4",
});
</script>
<div>
<footer class="footer">
<hr/>
&copy; 2002&ndash;2023 the Common Lisp Cookbook Project
<div>
2024-05-15 18:18:38 +02:00
📹 Discover <a style="color: darkgrey; text-decoration: underline", href="https://www.udemy.com/course/common-lisp-programming/?referralCode=2F3D698BBC4326F94358">our contributor's Common Lisp video course on Udemy</a>
2023-10-25 11:23:21 +02:00
</div>
</footer>
</div>
<div id="toc-btn">T<br>O<br>C</div>
</div>
<script text="javascript">
HighlightLisp.highlight_auto({className: null});
</script>
<script type="text/javascript">
function duckSearch() {
var searchField = document.getElementById("searchField");
if (searchField && searchField.value) {
var query = escape("site:lispcookbook.github.io/cl-cookbook/ " + searchField.value);
window.location.href = "https://duckduckgo.com/?kj=b2&kf=-1&ko=1&q=" + query;
// https://duckduckgo.com/params
// kj=b2: blue header in results page
// kf=-1: no favicons
}
}
</script>
<script async defer data-domain="lispcookbook.github.io/cl-cookbook" src="https://plausible.io/js/plausible.js"></script>
</body>
</html>