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

623 lines
26 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Linux version 5.2.0">
<title>Interfacing with your OS</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; Interfacing with your OS</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; Interfacing with your OS</h1>
<!-- Announcement we can keep for 1 month or more. I remove it and re-add it from time to time. -->
<!-- <p class="announce"> -->
<!-- 📢 🤶 â­<C3A2> -->
<!-- <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>
<p class="announce-neutral">
📕 <a href="index.html#download-in-epub">Get the EPUB and PDF</a>
</p>
<div id="content"
<p>The ANSI Common Lisp standard doesn’t mention this topic. (Keep in mind that it was written at a time where <a href="https://en.wikipedia.org/wiki/Lisp_machine">Lisp Machines</a> were at their peak. On these boxes Lisp <em>was</em> your operating system!) So almost everything that can be said here depends on your OS and your implementation.
There are, however, some widely used libraries, which either come with your Common Lisp implementation, or are easily
available through <a href="https://www.quicklisp.org/beta/">Quicklisp</a>. These include:</p>
<ul>
<li>ASDF3, which is included with almost all Common Lisp implementations,
includes <a href="https://common-lisp.net/project/asdf/uiop.html">Utilities for Implementation- and OS- Portability (UIOP)</a>.</li>
<li><a href="https://common-lisp.net/project/osicat/">osicat</a></li>
<li><a href="http://quickdocs.org/unix-opts/">unix-opts</a> or the newer <a href="https://github.com/dnaeon/clingon">clingon</a> are a command-line argument parsers, similar to Python’s <code>argparse</code>.</li>
</ul>
<p><a name="env"></a></p>
<h2 id="accessing-environment-variables">Accessing Environment variables</h2>
<p>UIOP comes with a function that’ll allow you to look at Unix/Linux environment variables on a lot of different CL implementations:</p>
<pre><code class="language-lisp">* (uiop:getenv "HOME")
"/home/edi"
</code></pre>
<p>Below is an example implementation, where we can see /feature flags/ used to run code on specific implementations:</p>
<pre><code class="language-lisp">* (defun my-getenv (name &amp;optional default)
"Obtains the current value of the POSIX environment variable NAME."
(declare (type (or string symbol) name))
(let ((name (string name)))
(or #+abcl (ext:getenv name)
#+ccl (ccl:getenv name)
#+clisp (ext:getenv name)
#+cmu (unix:unix-getenv name) ; since CMUCL 20b
#+ecl (si:getenv name)
#+gcl (si:getenv name)
#+mkcl (mkcl:getenv name)
#+sbcl (sb-ext:posix-getenv name)
default)))
MY-GETENV
* (my-getenv "HOME")
"/home/edi"
* (my-getenv "HOM")
NIL
* (my-getenv "HOM" "huh?")
"huh?"
</code></pre>
<p>You should also note that some of these implementations also provide the ability to <em>set</em> these variables. These include ECL (<code>si:setenv</code>) and AllegroCL, LispWorks, and CLISP where you can use the functions from above together with <a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_setf_.htm"><code>setf</code></a>. This feature might be important if you want to start subprocesses from your Lisp environment.</p>
<p>To set an envionmental variable, you can <code>setf</code> with <code>(uiop:getenv "lisp")</code> in a implementation-independent way.</p>
<p>Also note that the
<a href="https://www.common-lisp.net/project/osicat/manual/osicat.html#Environment">Osicat</a>
library has the method <code>(environment-variable "name")</code>, on POSIX-like
systems including Windows. It is also <code>fset</code>-able.</p>
<p><a name="accessing-command-line"></a></p>
<h2 id="accessing-the-command-line-arguments">Accessing the command line arguments</h2>
<h3 id="basics">Basics</h3>
<p>Accessing command line arguments is implementation-specific but it
appears most implementations have a way of getting at
them. UIOP with <code>uiop:command-line-arguments</code> or <a href="https://github.com/roswell/roswell/wiki">Roswell</a> as well as external
libraries (see next section) make it portable.</p>
<p><a href="http://www.sbcl.org">SBCL</a> stores the arguments list in the special variable <code>sb-ext:*posix-argv*</code></p>
<pre><code class="language-lisp">$ sbcl my-command-line-arg
</code></pre>
<p>….</p>
<pre><code class="language-lisp">* sb-ext:*posix-argv*
("sbcl" "my-command-line-arg")
*
</code></pre>
<p>More on using this to write standalone Lisp scripts can be found in the <a href="http://www.sbcl.org/manual/index.html#Command_002dline-arguments">SBCL Manual</a></p>
<p><a href="http://www.lispworks.com">LispWorks</a> has <code>system:*line-arguments-list*</code></p>
<pre><code class="language-lisp">* system:*line-arguments-list*
("/Users/cbrown/Projects/lisptty/tty-lispworks" "-init" "/Users/cbrown/Desktop/lisp/lispworks-init.lisp")
</code></pre>
<p>Here’s a quick function to return the argument strings list across multiple implementations:</p>
<pre><code class="language-lisp">(defun my-command-line ()
(or
#+SBCL *posix-argv*
#+LISPWORKS system:*line-arguments-list*)
#+CLISP *args*)
</code></pre>
<p>Now it would be handy to access them in a portable way and to parse
them according to a schema definition.</p>
<h3 id="parsing-command-line-arguments">Parsing command line arguments</h3>
<p>We have a look at the
<a href="https://github.com/CodyReichert/awesome-cl#scripting">Awesome CL list#scripting</a>
section and we’ll show how to use <a href="https://github.com/dnaeon/clingon">clingon</a>.</p>
<p>Please see our <a href="scripting.html#parsing-command-line-arguments">scripting recipe</a>.</p>
<h2 id="running-external-programs">Running external programs</h2>
<p><strong>uiop</strong> has us covered, and is probably included in your Common Lisp
implementation.</p>
<h3 id="synchronously">Synchronously</h3>
<p><a href="https://common-lisp.net/project/asdf/uiop.html#UIOP_002fRUN_002dPROGRAM"><code>uiop:run-program</code></a> either takes a string as argument, denoting the
name of the executable to run, or a list of strings, for the program and its arguments:</p>
<pre><code class="language-lisp">(uiop:run-program "firefox")
</code></pre>
<p>or</p>
<pre><code class="language-lisp">(uiop:run-program (list "firefox" "http:url"))
</code></pre>
<p>This will process the program output as specified and return the
processing results when the program and its output processing are
complete.</p>
<p>Use <code>:output t</code> to print to standard output.</p>
<p>This function has the following optional arguments:</p>
<pre><code class="language-lisp">run-program (command &amp;rest keys &amp;key
ignore-error-status
(force-shell nil force-shell-suppliedp)
input
(if-input-does-not-exist :error)
output
(if-output-exists :supersede)
error-output
(if-error-output-exists :supersede)
(element-type *default-stream-element-type*)
(external-format *utf-8-external-format*)
allow-other-keys)
</code></pre>
<p>It will always call a shell (rather than directly executing the command when possible)
if <code>force-shell</code> is specified. Similarly, it will never call a shell if <code>force-shell</code> is
specified to be <code>nil</code>.</p>
<p>Signal a continuable <code>subprocess-error</code> if the process wasn’t successful (exit-code 0),
unless <code>ignore-error-status</code> is specified.</p>
<p>If <code>output</code> is a pathname, a string designating a pathname, or <code>nil</code> (the default)
designating the null device, the file at that path is used as output.
If it’s <code>:interactive</code>, output is inherited from the current process;
beware that this may be different from your <code>*standard-output*</code>,
and under <code>slime</code> will be on your <code>*inferior-lisp*</code> buffer.
If it’s <code>t</code>, output goes to your current <code>*standard-output*</code> stream.
Otherwise, <code>output</code> should be a value that is a suitable first argument to
<code>slurp-input-stream</code> (qv.), or a list of such a value and keyword arguments.
In this case, <code>run-program</code> will create a temporary stream for the program output;
the program output, in that stream, will be processed by a call to <code>slurp-input-stream</code>,
using <code>output</code> as the first argument (or the first element of <code>output</code>, and the rest as keywords).
The primary value resulting from that call (or <code>nil</code> if no call was needed)
will be the first value returned by <code>run-program.</code>
E.g., using <code>:output :string</code> will have it return the entire output stream as a string.
And using <code>:output '(:string :stripped t</code>) will have it return the same string
stripped of any ending newline.</p>
<p><code>if-output-exists</code>, which is only meaningful if <code>output</code> is a string or a
pathname, can take the values <code>:error</code>, <code>:append</code>, and <code>:supersede</code> (the
default). The meaning of these values and their effect on the case
where <code>output</code> does not exist, is analogous to the <code>if-exists</code> parameter
to <code>open</code> with <code>:direction</code> <code>:output</code>.</p>
<p><code>error-output</code> is similar to <code>output</code>, except that the resulting value is returned
as the second value of <code>run-program</code>. t designates the <code>*error-output*</code>.
Also <code>:output</code> means redirecting the error output to the output stream,
in which case <code>nil</code> is returned.</p>
<p><code>if-error-output-exists</code> is similar to <code>if-output-exist</code>, except that it
affects <code>error-output</code> rather than <code>output</code>.</p>
<p><code>input</code> is similar to <code>output</code>, except that <code>vomit-output-stream</code> is used,
no value is returned, and T designates the <code>*standard-input*</code>.</p>
<p><code>if-input-does-not-exist</code>, which is only meaningful if <code>input</code> is a string
or a pathname, can take the values <code>:create</code> and <code>:error</code> (the
default). The meaning of these values is analogous to the
<code>if-does-not-exist</code> parameter to <code>open</code> with <code>:direction :input</code>.</p>
<p><code>element-type</code> and <code>external-format</code> are passed on
to your Lisp implementation, when applicable, for creation of the output stream.</p>
<p>One and only one of the stream slurping or vomiting may or may not happen
in parallel in parallel with the subprocess,
depending on options and implementation,
and with priority being given to output processing.
Other streams are completely produced or consumed
before or after the subprocess is spawned, using temporary files.</p>
<p><code>run-program</code> returns 3 values:</p>
<ul>
<li>the result of the <code>output</code> slurping if any, or <code>nil</code></li>
<li>the result of the <code>error-output</code> slurping if any, or <code>nil</code></li>
<li>either 0 if the subprocess exited with success status, or an
indication of failure via the <code>exit-code</code> of the process</li>
</ul>
<h3 id="asynchronously">Asynchronously</h3>
<p>With <a href="https://common-lisp.net/project/asdf/uiop.html#UIOP_002fLAUNCH_002dPROGRAM"><code>uiop:launch-program</code></a>.</p>
<p>Its signature is the following:</p>
<pre><code class="language-lisp">launch-program (command &amp;rest keys &amp;key
input
(if-input-does-not-exist :error)
output
(if-output-exists :supersede)
error-output
(if-error-output-exists :supersede)
(element-type *default-stream-element-type*)
(external-format *utf-8-external-format*)
directory
#+allegro separate-streams
&amp;allow-other-keys)
</code></pre>
<p>Output (stdout) from the launched program is set using the <code>output</code>
keyword:</p>
<ul>
<li>If <code>output</code> is a pathname, a string designating a pathname, or
<code>nil</code> (the default) designating the null device, the file at that
path is used as output.</li>
<li>If it’s <code>:interactive</code>, output is inherited from the current process;
beware that this may be different from your <code>*standard-output*</code>, and
under Slime will be on your <code>*inferior-lisp*</code> buffer.</li>
<li>If it’s <code>T</code>, output goes to your current <code>*standard-output*</code> stream.</li>
<li>If it’s <code>:stream</code>, a new stream will be made available that can be accessed via
<code>process-info-output</code> and read from.</li>
<li>Otherwise, <code>output</code> should be a value that the underlying lisp
implementation knows how to handle.</li>
</ul>
<p><code>if-output-exists</code>, which is only meaningful if <code>output</code> is a string or a
pathname, can take the values <code>:error</code>, <code>:append</code>, and <code>:supersede</code> (the
default). The meaning of these values and their effect on the case
where <code>output</code> does not exist, is analogous to the <code>if-exists</code> parameter
to <code>open</code> with <code>:DIRECTION :output</code>.</p>
<p><code>error-output</code> is similar to <code>output</code>. T designates the <code>*error-output*</code>,
<code>:output</code> means redirecting the error output to the output stream,
and <code>:stream</code> causes a stream to be made available via
<code>process-info-error-output</code>.</p>
<p><code>launch-program</code> returns a <code>process-info</code> object, which look like the following (<a href="https://gitlab.common-lisp.net/asdf/asdf/blob/master/uiop/launch-program.lisp#L205">source</a>):</p>
<pre><code class="language-lisp">(defclass process-info ()
(
;; The advantage of dealing with streams instead of PID is the
;; availability of functions like `sys:pipe-kill-process`.
(process :initform nil)
(input-stream :initform nil)
(output-stream :initform nil)
(bidir-stream :initform nil)
(error-output-stream :initform nil)
;; For backward-compatibility, to maintain the property (zerop
;; exit-code) &lt;-&gt; success, an exit in response to a signal is
;; encoded as 128+signum.
(exit-code :initform nil)
;; If the platform allows it, distinguish exiting with a code
;; &gt;128 from exiting in response to a signal by setting this code
(signal-code :initform nil)))
</code></pre>
<p>See the <a href="https://gitlab.common-lisp.net/asdf/asdf/blob/master/uiop/launch-program.lisp#L508">docstrings</a>.</p>
<h4 id="test-if-a-subprocess-is-alive">Test if a subprocess is alive</h4>
<p><code>uiop:process-alive-p</code> tests if a process is still alive, given a
<code>process-info</code> object returned by <code>launch-program</code>:</p>
<pre><code class="language-lisp">* (defparameter *shell* (uiop:launch-program "bash"
:input :stream :output :stream))
;; inferior shell process now running
* (uiop:process-alive-p *shell*)
T
;; Close input and output streams
* (uiop:close-streams *shell*)
* (uiop:process-alive-p *shell*)
NIL
</code></pre>
<h4 id="get-the-exit-code">Get the exit code</h4>
<p>We can use <code>uiop:wait-process</code>. If the process is finished, it returns
immediately, and returns the exit code. If not, it waits for the
process to terminate.</p>
<pre><code class="language-lisp">(uiop:process-alive-p *process*)
NIL
(uiop:wait-process *process*)
0
</code></pre>
<p>An exit code to 0 means success (use <code>zerop</code>).</p>
<p>The exit code is also stored in the <code>exit-code</code> slot of our
<code>process-info</code> object. We see from the class definition above that it
has no accessor, so we’ll use <code>slot-value</code>. It has an <code>initform</code> to
nil, so we don’t have to check if the slot is bound. We can do:</p>
<pre><code class="language-lisp">(slot-value *my-process* 'uiop/launch-program::exit-code)
0
</code></pre>
<p>The trick is that we <em>must</em> run <code>wait-process</code> beforehand, otherwise
the result will be <code>nil</code>.</p>
<p>Since <code>wait-process</code> is blocking, we can do it on a new thread:</p>
<pre><code class="language-lisp">(bt:make-thread
(lambda ()
(let ((exit-code (uiop:wait-process
(uiop:launch-program (list "of" "commands"))))
(if (zerop exit-code)
(print :success)
(print :failure)))))
:name "Waiting for &lt;program&gt;")
</code></pre>
<p>Note that <code>run-program</code> returns the exit code as the third value.</p>
<h3 id="input-and-output-from-subprocess">Input and output from subprocess</h3>
<p>If the <code>input</code> keyword is set to <code>:stream</code>, then a stream is created
and can be written to in the same way as a file. The stream can be
accessed using <code>uiop:process-info-input</code>:</p>
<pre><code class="language-lisp">;; Start the inferior shell, with input and output streams
* (defparameter *shell* (uiop:launch-program "bash"
:input :stream :output :stream))
;; Write a line to the shell
* (write-line "find . -name '*.md'"
(uiop:process-info-input *shell*))
;; Flush stream
* (force-output (uiop:process-info-input *shell*))
</code></pre>
<p>where <a href="http://clhs.lisp.se/Body/f_wr_stg.htm">write-line</a> writes the
string to the given stream, adding a newline at the end. The
<a href="http://clhs.lisp.se/Body/f_finish.htm">force-output</a> call attempts to
flush the stream, but does not wait for completion.</p>
<p>Reading from the output stream is similar, with
<code>uiop:process-info-output</code> returning the output stream:</p>
<pre><code class="language-lisp">* (read-line (uiop:process-info-output *shell*))
</code></pre>
<p>In some cases the amount of data to be read is known, or there are
delimiters to determine when to stop reading. If this is not the case,
then calls to <a href="http://clhs.lisp.se/Body/f_rd_lin.htm">read-line</a> can
hang while waiting for data. To avoid this,
<a href="http://clhs.lisp.se/Body/f_listen.htm">listen</a> can be used to test if
a character is available:</p>
<pre><code class="language-lisp">* (let ((stream (uiop:process-info-output *shell*)))
(loop while (listen stream) do
;; Characters are immediately available
(princ (read-line stream))
(terpri)))
</code></pre>
<p>There is also
<a href="http://clhs.lisp.se/Body/f_rd_c_1.htm">read-char-no-hang</a> which reads
a single character, or returns <code>nil</code> if no character is available.
Note that due to issues like buffering, and the timing of when the
other process is executed, there is no guarantee that all data sent
will be received before <code>listen</code> or <code>read-char-no-hang</code> return <code>nil</code>.</p>
<h3 id="capturing-standard-and-error-output">Capturing standard and error output</h3>
<p>Capturing standard output, as seen above, is easily done by telling
<code>:output</code> to be <code>:string</code>, or using <code>:output '(:string :stripped t)</code> to
strip any ending newline.</p>
<p>You can ask the same to <code>:error-output</code> and, in addition, you can ask
<code>uiop:run-program</code> to <em>not</em> signal an error, thus to not enter the
interactive debugger, with <code>:ignore-error-status t</code>.</p>
<p>In that case, you can check the success or the failure of the program
with the returned <code>exit-code</code>. 0 is success.</p>
<p>Here’s everything together:</p>
<pre><code class="language-lisp">(uiop:run-program (list "git"
"checkout"
"me/does-not-exist")
:output :string
:error-output :string
:ignore-error-status t)
;; =&gt;
""
"error: pathspec 'me/does-not-exist did not match any file(s) known to git
"
1
</code></pre>
<p><code>uiop:run-program</code> returns 3 values:</p>
<ul>
<li>the standard output (here, as a blank string)</li>
<li>the error output (here, as a string with our error message)</li>
<li>the exit code</li>
</ul>
<p>We can bind them with <code>multiple-value-bind</code>:</p>
<pre><code class="language-lisp">(multiple-value-bind (output error-output exit-code)
(uiop:run-program (list …))
(unless (zerop exit-code)
(format t "error output is: ~a" error-output)))
</code></pre>
<h3 id="running-interactive-and-visual-commands-htop">Running interactive and visual commands (htop)</h3>
<p>Use <code>uiop:run-program</code> and set both <code>:input</code> and <code>:output</code> to <code>:interactive</code>:</p>
<pre><code class="language-lisp">(uiop:run-program "htop"
:output :interactive
:input :interactive)
</code></pre>
<p>This will spawn <code>htop</code> in full screen, as it should.</p>
<p>It works for more commands (<code>sudo</code>, <code>vim</code>, <code>less</code>…).</p>
<h2 id="piping">Piping</h2>
<p>Here’s an example to do the equivalent of <code>ls | sort</code>. Note that “lsâ€<C3A2>
uses <code>launch-program</code> (async) and outputs to a stream, where “sortâ€<C3A2>,
the last command of the pipe, uses <code>run-program</code> and outputs to a
string.</p>
<pre><code class="language-lisp">(uiop:run-program "sort"
:input
(uiop:process-info-output
(uiop:launch-program "ls"
:output :stream))
:output :string)
</code></pre>
<h2 id="get-lisps-current-process-id-pid">Get Lisp’s current Process ID (PID)</h2>
<p>Implementations provide their own functions for this.</p>
<p>On SBCL:</p>
<pre><code class="language-lisp">(sb-posix:getpid)
</code></pre>
<p>It is possible portably with the osicat library:</p>
<pre><code class="language-lisp">(osicat-posix:getpid)
</code></pre>
<p>Here again, we could find it by using the <code>apropos</code> function:</p>
<pre><code class="language-lisp">CL-USER&gt; (apropos "pid")
OSICAT-POSIX:GETPID (fbound)
OSICAT-POSIX::PID
[…]
SB-IMPL::PID
SB-IMPL::WAITPID (fbound)
SB-POSIX:GETPID (fbound)
SB-POSIX:GETPPID (fbound)
SB-POSIX:LOG-PID (bound)
SB-POSIX::PID
SB-POSIX::PID-T
SB-POSIX:WAITPID (fbound)
[…]
</code></pre>
<p class="page-source">
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/os.md">os.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>
📹 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>
</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>