833 lines
34 KiB
HTML
833 lines
34 KiB
HTML
<!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> – 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> – 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. -->
|
||
<p class="announce">
|
||
📹 💻
|
||
<a style="font-size: 120%" href="https://www.udemy.com/course/common-lisp-programming/?couponCode=LISPMACROSPOWER" 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 vindarel's Lisp course in videos with this September 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-neutral">
|
||
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
|
||
</p>
|
||
|
||
|
||
<div id="content"
|
||
<p>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.</p>
|
||
|
||
<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 doesn’t 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>
|
||
|
||
<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>
|
||
|
||
<pre><code class="language-lisp">(sb-ext:save-lisp-and-die #P"path/name-of-executable" :toplevel #'my-app:main-function :executable t)
|
||
</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 program’s entry point, here <code>my-app:main-function</code>. Don’t 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, you’ll 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 project’s .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")
|
||
(sb-ext:save-lisp-and-die #p"my-app-binary" :toplevel #'my-app:main :executable t)
|
||
</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 we’ve 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 "<here your final binary name>"
|
||
:entry-point "<my-package:main-function>"
|
||
</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 else’s 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, you’ll see it uses the shipped libraries:</p>
|
||
|
||
<pre><code class="language-lisp">$ ./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.
|
||
[…]
|
||
</code></pre>
|
||
|
||
<p>Success!</p>
|
||
|
||
<p>A note regarding <code>libssl</code>. 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 <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 doesn’t find, you can instruct it like this:</p>
|
||
|
||
<pre><code class="language-lisp">(deploy:define-library cl+ssl::libcrypto
|
||
;; ^^^ CFFI system name. Find it with a call to "apropos".
|
||
: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 Deploy’s 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 Deploy’s 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 Roswell’s 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 <app.ros> --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>We’ll 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 shouldn’t 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".
|
||
(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.~&")
|
||
(clack:stop *server*)
|
||
(uiop:quit)))
|
||
(error (c) (format t "Woops, an unknown error occured:~&~a~&" 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>
|
||
|
||
<p>SBCL isn’t the only Lisp implementation.
|
||
<a href="https://gitlab.com/embeddable-common-lisp/ecl/">ECL</a>, Embeddable
|
||
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>CCL’s 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>
|
||
|
||
<p>You’ll also want to investigate the proprietary Lisps’ tree shakers capabilities.</p>
|
||
|
||
<p>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.</p>
|
||
|
||
<h3 id="building-a-smaller-binary-with-sbcls-core-compression">Building a smaller binary with SBCL’s core compression</h3>
|
||
|
||
<p>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.</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))
|
||
(uiop:dump-image (asdf:output-file o c) :executable t :compression t))
|
||
</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>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.</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>Let’s 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, let’s create options. Clingon already handles “–help” for us, but not the short version. Here’s how we use <code>clingon:make-option</code> to create an option:</p>
|
||
|
||
<pre><code class="language-lisp">(clingon:make-option
|
||
:flag ;; <--- option kind. A "flag" does not expect a parameter on the CLI.
|
||
:description "short help"
|
||
;; :long-name "help" ;; <--- long name, sans the "--" prefix, but here it's a duplicate.
|
||
:short-name #\h ;; <--- short name, a character
|
||
;; :required t ;; <--- is this option always required? In our case, no.
|
||
:key :help) ;; <--- 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
|
||
option’s value will be truthy, otherwise it will be falsy. A flag does
|
||
not expect an argument, it’s 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>We’ll 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 ;; <--- string type: expects one parameter on the CLI.
|
||
:description "Name to greet"
|
||
:short-name #\n
|
||
:long-name "name"
|
||
:env-vars '("USER") ;; <-- takes this default value if the env var exists.
|
||
:initial-value "lisper" ;; <-- 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 app’s logic.</li>
|
||
</ul>
|
||
|
||
<p>And finally, we’ll use <code>clingon:run</code> in our main function (the entry
|
||
point of our binary) to parse the command-line arguments, and apply
|
||
our command’s logic. During development, we can also manually call
|
||
<code>clingon:parse-command-line</code> to try things out.</p>
|
||
|
||
<p>Here’s a minimal command. We’ll 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 <john.doe@example.org")
|
||
:license "BSD 2-Clause"
|
||
:options (cli/options) ;; <-- our options
|
||
:handler #'null)) ;; <-- 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> (clingon:parse-command-line (cli/command) '("-h" "-n" "me"))
|
||
#<CLINGON.COMMAND:COMMAND name=hello options=5 sub-commands=0>
|
||
</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>Let’s try again with an unknown option:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (clingon:parse-command-line (cli/command) '("-x"))
|
||
;; => 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 Clingon’s conditions with
|
||
<code>handler-bind</code> and use its restarts, to do something different with
|
||
unknown options. But we don’t 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 tool’s usage information:</p>
|
||
|
||
<pre><code>CL-USER> (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 <VALUE> Name to greet [default: lisper] [env: $USER]
|
||
|
||
AUTHORS:
|
||
John Doe <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 option’s 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>Here’s how to use them:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (let ((command (clingon:parse-command-line (cli/command) '("-n" "you" "last"))))
|
||
(format t "name is: ~a~&" (clingon:getopt command :name))
|
||
(format t "free args are: ~s~&" (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))) ;; <-- using the option's :key
|
||
(format t "Hello, ~a!~%" name)
|
||
(format t "You have provided ~a more free arguments~%" (length free-args))
|
||
(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)) ;; <-- changed.
|
||
</code></pre>
|
||
|
||
<p>We now only have to write the main entry point of our binary and we’re done.</p>
|
||
|
||
<p>By the way, <code>clingon:getopt</code> returns 3 values:</p>
|
||
|
||
<ul>
|
||
<li>the option’s 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 that’s about it. Congratulations, you can now properly parse command-line arguments!</p>
|
||
|
||
<p>Go check Clingon’s 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>. Let’s read the stacktrace:</p>
|
||
|
||
<pre><code>$ ./my-app
|
||
sleep…
|
||
^C
|
||
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.
|
||
</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)
|
||
(sb-sys:interactive-interrupt () (progn
|
||
(format *error-output* "Abort.~&")
|
||
(opts:exit))))
|
||
</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. There’s 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/>
|
||
© 2002–2023 the Common Lisp Cookbook Project
|
||
<div>
|
||
📹 Discover <a style="color: darkgrey; text-decoration: underline", href="https://www.udemy.com/course/common-lisp-programming/?referralCode=2F3D698BBC4326F94358">vindarel's Lisp course on Udemy</a>
|
||
</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>
|