926 lines
36 KiB
HTML
926 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta name="generator" content=
|
||
"HTML Tidy for HTML5 for Linux version 5.2.0">
|
||
<title>Files and Directories</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> – Files and Directories</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> – Files and Directories</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=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>We’ll see here a handful of functions and libraries to operate on files and directories.</p>
|
||
|
||
<p>In this chapter, we use mainly
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/19_aa.htm">namestrings</a>
|
||
to
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/19_.htm">specify filenames</a>.
|
||
In a recipe or two we also use
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/19_ab.htm">pathnames</a>.</p>
|
||
|
||
<p>Many functions will come from UIOP, so we suggest you have a look directly at it:</p>
|
||
|
||
<ul>
|
||
<li><a href="https://common-lisp.net/project/asdf/uiop.html#UIOP_002fFILESYSTEM">UIOP/filesystem</a></li>
|
||
<li><a href="https://common-lisp.net/project/asdf/uiop.html#UIOP_002fPATHNAME">UIOP/pathname</a></li>
|
||
</ul>
|
||
|
||
<p>Of course, do not miss:</p>
|
||
|
||
<ul>
|
||
<li><a href="http://gigamonkeys.com/book/files-and-file-io.html">Files and File I/O in Practical Common Lisp</a></li>
|
||
</ul>
|
||
|
||
<h3 id="getting-the-components-of-a-pathname">Getting the components of a pathname</h3>
|
||
|
||
<h4 id="file-name-sans-directory">File name (sans directory)</h4>
|
||
|
||
<p>Use <code>file-namestring</code> to get a file name from a pathname:</p>
|
||
|
||
<pre><code class="language-lisp">(file-namestring #p"/path/to/file.lisp") ;; => "file.lisp"
|
||
</code></pre>
|
||
|
||
<h4 id="file-extension">File extension</h4>
|
||
|
||
<p>The file extension is called “pathname type” in Lisp parlance:</p>
|
||
|
||
<pre><code class="language-lisp">(pathname-type "~/foo.org") ;; => "org"
|
||
</code></pre>
|
||
|
||
<h4 id="file-basename">File basename</h4>
|
||
|
||
<p>The basename is called the “pathname name” -</p>
|
||
|
||
<pre><code class="language-lisp">(pathname-name "~/foo.org") ;; => "foo"
|
||
(pathname-name "~/foo") ;; => "foo"
|
||
</code></pre>
|
||
|
||
<p>If a directory pathname has a trailing slash, <code>pathname-name</code> may return <code>nil</code>; use <code>pathname-directory</code> instead -</p>
|
||
|
||
<pre><code class="language-lisp">(pathname-name "~/foo/") ;; => NIL
|
||
(first (last (pathname-directory #P"~/foo/"))) ;; => "foo"
|
||
</code></pre>
|
||
|
||
<h4 id="parent-directory">Parent directory</h4>
|
||
|
||
<pre><code class="language-lisp">(uiop:pathname-parent-directory-pathname #P"/foo/bar/quux/")
|
||
;; => #P"/foo/bar/"
|
||
</code></pre>
|
||
|
||
<h3 id="testing-whether-a-file-exists">Testing whether a file exists</h3>
|
||
|
||
<p>Use the function
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_probe_.htm"><code>probe-file</code></a>
|
||
which will return a
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_g.htm#generalized_boolean">generalized boolean</a> -
|
||
either <code>nil</code> if the file doesn’t exists, or its
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/20_ac.htm">truename</a>
|
||
(which might be different from the argument you supplied).</p>
|
||
|
||
<p>For more portability, use <code>uiop:probe-file*</code> or <code>uiop:file-exists-p</code>
|
||
which will return the file pathname (if it exists).</p>
|
||
|
||
<pre><code class="language-lisp">$ ln -s /etc/passwd foo
|
||
|
||
* (probe-file "/etc/passwd")
|
||
#p"/etc/passwd"
|
||
|
||
* (probe-file "foo")
|
||
#p"/etc/passwd"
|
||
|
||
* (probe-file "bar")
|
||
NIL
|
||
</code></pre>
|
||
|
||
<h3 id="expanding-a-file-or-a-directory-name-with-a-tilde-">Expanding a file or a directory name with a tilde (<code>~</code>)</h3>
|
||
|
||
<p>For portability, use <code>uiop:native-namestring</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(uiop:native-namestring "~/.emacs.d/")
|
||
"/home/me/.emacs.d/"
|
||
</code></pre>
|
||
|
||
<p>It also expand the tilde with files and directories that don’t exist:</p>
|
||
|
||
<pre><code class="language-lisp">(uiop:native-namestring "~/foo987.txt")
|
||
:: "/home/me/foo987.txt"
|
||
</code></pre>
|
||
|
||
<p>On several implementations (CCL, ABCL, ECL, CLISP, LispWorks),
|
||
<code>namestring</code> works similarly. On SBCL, if the file or directory
|
||
doesn’t exist, <code>namestring</code> doesn’t expand the path but returns the
|
||
argument, with the tilde.</p>
|
||
|
||
<p>With files that exist, you can also use <code>truename</code>. But, at least on
|
||
SBCL, it returns an error if the path doesn’t exist.</p>
|
||
|
||
<h3 id="turning-a-pathname-into-a-string-with-windows-directory-separator">Turning a pathname into a string with Windows’ directory separator</h3>
|
||
|
||
<p>Use again <code>uiop:native-namestring</code>:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (uiop:native-namestring #p"~/foo/")
|
||
"C:\\Users\\You\\foo\\"
|
||
</code></pre>
|
||
|
||
<p>See also <code>uiop:parse-native-namestring</code> for the inverse operation.</p>
|
||
|
||
<h3 id="creating-directories">Creating directories</h3>
|
||
|
||
<p>The function
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_ensu_1.htm">ensure-directories-exist</a>
|
||
creates the directories if they do not exist:</p>
|
||
|
||
<pre><code class="language-lisp">(ensure-directories-exist "foo/bar/baz/")
|
||
</code></pre>
|
||
|
||
<p>This may create <code>foo</code>, <code>bar</code> and <code>baz</code>. Don’t forget the trailing slash.</p>
|
||
|
||
<h3 id="deleting-directories">Deleting directories</h3>
|
||
|
||
<p>Use <code>uiop:delete-directory-tree</code> with a pathname (<code>#p</code>), a trailing slash and the <code>:validate</code> key:</p>
|
||
|
||
<pre><code class="language-lisp">;; mkdir dirtest
|
||
(uiop:delete-directory-tree #p"dirtest/" :validate t)
|
||
</code></pre>
|
||
|
||
<p>You can use <code>pathname</code> around a string that designates a directory:</p>
|
||
|
||
<pre><code class="language-lisp">(defun rmdir (path)
|
||
(uiop:delete-directory-tree (pathname path) :validate t))
|
||
</code></pre>
|
||
|
||
<p>UIOP also has <code>delete-empty-directory</code></p>
|
||
|
||
<p><a href="https://edicl.github.io/cl-fad/">cl-fad</a> has <code>(fad:delete-directory-and-files "dirtest")</code>.</p>
|
||
|
||
<h3 id="merging-files-and-directories">Merging files and directories</h3>
|
||
|
||
<p>Use <code>merge-pathnames</code>, with one thing to note: if you want to append
|
||
directories, the second argument must have a trailing <code>/</code>.</p>
|
||
|
||
<p>As always, look at UIOP functions. We have a <code>uiop:merge-pathnames*</code>
|
||
equivalent which fixes corner cases.</p>
|
||
|
||
<p>So, here’s how to append a directory to another one:</p>
|
||
|
||
<pre><code class="language-lisp">(merge-pathnames "otherpath" "/home/vince/projects/")
|
||
;; important: ^^
|
||
;; a trailing / denotes a directory.
|
||
;; => #P"/home/vince/projects/otherpath"
|
||
</code></pre>
|
||
|
||
<p>Look at the difference: if you don’t include a trailing slash to
|
||
either paths, <code>otherpath</code> and <code>projects</code> are seen as files, so <code>otherpath</code> is appended to the base directory containing <code>projects</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(merge-pathnames "otherpath" "/home/vince/projects")
|
||
;; #P"/home/vince/otherpath"
|
||
;; ^^ no "projects", because it was seen as a file.
|
||
</code></pre>
|
||
|
||
<p>or again, with <code>otherpath/</code> (a trailing <code>/</code>) but <code>projects</code> seen as a file:</p>
|
||
|
||
<pre><code class="language-lisp">(merge-pathnames "otherpath/" "/home/vince/projects")
|
||
;; #P"/home/vince/otherpath/projects"
|
||
;; ^^ inserted here
|
||
</code></pre>
|
||
|
||
<h3 id="get-the-current-working-directory-cwd">Get the current working directory (CWD)</h3>
|
||
|
||
<p>Use <code>uiop/os:getcwd</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(uiop/os:getcwd)
|
||
;; #P"/home/vince/projects/cl-cookbook/"
|
||
;; ^ with a trailing slash, useful for merge-pathnames
|
||
</code></pre>
|
||
|
||
<h3 id="get-the-current-directory-relative-to-a-lisp-project">Get the current directory relative to a Lisp project</h3>
|
||
|
||
<p>Use <code>asdf:system-relative-pathname system path</code>.</p>
|
||
|
||
<p>Say you are working inside <code>mysystem</code>. It has an ASDF system
|
||
declaration, the system is loaded in your Lisp image. This ASDF file
|
||
is somewhere on your filesystem and you want the path to <code>src/web/</code>. Do this:</p>
|
||
|
||
<pre><code class="language-lisp">(asdf:system-relative-pathname "mysystem" "src/web/")
|
||
;; => #P"/home/vince/projects/mysystem/src/web/"
|
||
</code></pre>
|
||
|
||
<p>This will work on another user’s machine, where the system sources are located in another location.</p>
|
||
|
||
<h3 id="setting-the-current-working-directory">Setting the current working directory</h3>
|
||
|
||
<p>Use <a href="https://asdf.common-lisp.dev/uiop.html#Function-uiop_002fos_003achdir"><code>uiop:chdir</code></a> <em><code>path</code></em>:</p>
|
||
|
||
<pre><code class="language-lisp">(uiop:chdir "/bin/")
|
||
0
|
||
</code></pre>
|
||
|
||
<p>The trailing slash in <em>path</em> is optional.</p>
|
||
|
||
<p>Or, to set for the current directory for the next operation only, use <code>uiop:with-current-directory</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(let ((dir "/path/to/another/directory/"))
|
||
(uiop:with-current-directory (dir)
|
||
(directory-files "./")))
|
||
</code></pre>
|
||
|
||
<h3 id="opening-a-file">Opening a file</h3>
|
||
|
||
<p>Common Lisp has
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_open.htm"><code>open</code></a> and
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_close.htm"><code>close</code></a>
|
||
functions which resemble the functions of the same denominator from other
|
||
programming languages you’re probably familiar with. However, it is almost
|
||
always recommendable to use the macro
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/m_w_open.htm"><code>with-open-file</code></a>
|
||
instead. Not only will this macro open the file for you and close it when you’re
|
||
done, it’ll also take care of it if your code leaves the body abnormally (such
|
||
as by a use of
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/s_throw.htm">throw</a>). A
|
||
typical use of <code>with-open-file</code> looks like this:</p>
|
||
|
||
<pre><code class="language-lisp">(with-open-file (str <_file-spec_>
|
||
:direction <_direction_>
|
||
:if-exists <_if-exists_>
|
||
:if-does-not-exist <_if-does-not-exist_>)
|
||
(your code here))
|
||
</code></pre>
|
||
|
||
<ul>
|
||
<li><code>str</code> is a variable which’ll be bound to the stream which is created by
|
||
opening the file.</li>
|
||
<li><code><_file-spec_></code> will be a truename or a pathname.</li>
|
||
<li><code><_direction_></code> is usually <code>:input</code> (meaning you want to read from the file),
|
||
<code>:output</code> (meaning you want to write to the file) or <code>:io</code> (which is for
|
||
reading <em>and</em> writing at the same time) - the default is <code>:input</code>.</li>
|
||
<li><code><_if-exists_></code> specifies what to do if you want to open a file for writing
|
||
and a file with that name already exists - this option is ignored if you
|
||
just want to read from the file. The default is <code>:error</code> which means that an
|
||
error is signalled. Other useful options are <code>:supersede</code> (meaning that the
|
||
new file will replace the old one), <code>:append</code> (content is added to the file),
|
||
<code>nil</code> (the stream variable will be bound to <code>nil</code>),
|
||
and <code>:rename</code> (i.e. the old file is renamed).</li>
|
||
<li><code><_if-does-not-exist_></code> specifies what to do if the file you want to open does
|
||
not exist. It is one of <code>:error</code> for signalling an error, <code>:create</code> for
|
||
creating an empty file, or <code>nil</code> for binding the stream variable to
|
||
<code>nil</code>. The default is, to be brief, to do the right thing depending on the
|
||
other options you provided. See the CLHS for details.</li>
|
||
</ul>
|
||
|
||
<p>Note that there are a lot more options to <code>with-open-file</code>. See
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_open.htm">the CLHS entry for <code>open</code></a>
|
||
for all the details. You’ll find some examples on how to use <code>with-open-file</code>
|
||
below. Also note that you usually don’t need to provide any keyword arguments if
|
||
you just want to open an existing file for reading.</p>
|
||
|
||
<h3 id="reading-files">Reading files</h3>
|
||
|
||
<h4 id="reading-a-file-into-a-string-or-a-list-of-lines">Reading a file into a string or a list of lines</h4>
|
||
|
||
<p>It’s quite common to need to access the contents of a file in string
|
||
form, or to get a list of lines.</p>
|
||
|
||
<p>uiop is included in ASDF (there is no extra library to install or
|
||
system to load) and has the following functions:</p>
|
||
|
||
<pre><code class="language-lisp">(uiop:read-file-string "file.txt")
|
||
</code></pre>
|
||
|
||
<p>and</p>
|
||
|
||
<pre><code class="language-lisp">(uiop:read-file-lines "file.txt")
|
||
</code></pre>
|
||
|
||
<p><em>Otherwise</em>, this can be achieved by using <code>read-line</code> or <code>read-char</code> functions,
|
||
that probably won’t be the best solution. The file might not be divided into
|
||
multiple lines or reading one character at a time might bring significant
|
||
performance problems. To solve this problems, you can read files using buckets
|
||
of specific sizes.</p>
|
||
|
||
<pre><code class="language-lisp">(with-output-to-string (out)
|
||
(with-open-file (in "/path/to/big/file")
|
||
(loop with buffer = (make-array 8192 :element-type 'character)
|
||
for n-characters = (read-sequence buffer in)
|
||
while (< 0 n-characters)
|
||
do (write-sequence buffer out :start 0 :end n-characters)))))
|
||
</code></pre>
|
||
|
||
<p>Furthermore, you’re free to change the format of the read/written data, instead
|
||
of using elements of type character every time. For instance, you can set
|
||
<code>:element-type</code> type argument of <code>with-output-to-string</code>, <code>with-open-file</code> and
|
||
<code>make-array</code> functions to <code>'(unsigned-byte 8)</code> to read data in octets.</p>
|
||
|
||
<h4 id="reading-with-an-utf-8-encoding">Reading with an utf-8 encoding</h4>
|
||
|
||
<p>To avoid an <code>ASCII stream decoding error</code> you might want to specify an UTF-8 encoding:</p>
|
||
|
||
<pre><code class="language-lisp">(with-open-file (in "/path/to/big/file"
|
||
:external-format :utf-8)
|
||
...
|
||
</code></pre>
|
||
|
||
<h4 id="set-sbcls-default-encoding-format-to-utf-8">Set SBCL’s default encoding format to utf-8</h4>
|
||
|
||
<p>Sometimes you don’t control the internals of a library, so you’d
|
||
better set the default encoding to utf-8. Add this line to your
|
||
<code>~/.sbclrc</code>:</p>
|
||
|
||
<pre><code>(setf sb-impl::*default-external-format* :utf-8)
|
||
</code></pre>
|
||
|
||
<p>and optionally</p>
|
||
|
||
<pre><code>(setf sb-alien::*default-c-string-external-format* :utf-8)
|
||
</code></pre>
|
||
|
||
<h4 id="reading-a-file-one-line-at-a-time">Reading a file one line at a time</h4>
|
||
|
||
<p><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_rd_lin.htm"><code>read-line</code></a>
|
||
will read one line from a stream (which defaults to
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_s.htm#standard_input"><em>standard input</em></a>)
|
||
the end of which is determined by either a newline character or the end of the
|
||
file. It will return this line as a string <em>without</em> the trailing newline
|
||
character. (Note that <code>read-line</code> has a second return value which is true if there
|
||
was no trailing newline, i.e. if the line was terminated by the end of the
|
||
file.) <code>read-line</code> will by default signal an error if the end of the file is
|
||
reached. You can inhibit this by supplying NIL as the second argument. If you do
|
||
this, <code>read-line</code> will return <code>nil</code> if it reaches the end of the file.</p>
|
||
|
||
<pre><code class="language-lisp">(with-open-file (stream "/etc/passwd")
|
||
(do ((line (read-line stream nil)
|
||
(read-line stream nil)))
|
||
((null line))
|
||
(print line)))
|
||
</code></pre>
|
||
|
||
<p>You can also supply a third argument which will be used instead of <code>nil</code> to signal
|
||
the end of the file:</p>
|
||
|
||
<pre><code class="language-lisp">(with-open-file (stream "/etc/passwd")
|
||
(loop for line = (read-line stream nil 'foo)
|
||
until (eq line 'foo)
|
||
do (print line)))
|
||
</code></pre>
|
||
|
||
<h4 id="reading-a-file-one-character-at-a-time">Reading a file one character at a time</h4>
|
||
|
||
<p><a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_rd_cha.htm"><code>read-char</code></a>
|
||
is similar to <code>read-line</code>, but it only reads one character as opposed to one
|
||
line. Of course, newline characters aren’t treated differently from other
|
||
characters by this function.</p>
|
||
|
||
<pre><code class="language-lisp">(with-open-file (stream "/etc/passwd")
|
||
(do ((char (read-char stream nil)
|
||
(read-char stream nil)))
|
||
((null char))
|
||
(print char)))
|
||
</code></pre>
|
||
|
||
<h4 id="looking-one-character-ahead">Looking one character ahead</h4>
|
||
|
||
<p>You can ‘look at’ the next character of a stream without actually removing it
|
||
from there - this is what the function
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_peek_c.htm"><code>peek-char</code></a>
|
||
is for. It can be used for three different purposes depending on its first
|
||
(optional) argument (the second one being the stream it reads from): If the
|
||
first argument is <code>nil</code>, <code>peek-char</code> will just return the next character that’s
|
||
waiting on the stream:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (with-input-from-string (stream "I'm not amused")
|
||
(print (read-char stream))
|
||
(print (peek-char nil stream))
|
||
(print (read-char stream))
|
||
(values))
|
||
|
||
#\I
|
||
#\'
|
||
#\'
|
||
</code></pre>
|
||
|
||
<p>If the first argument is <code>T</code>, <code>peek-char</code> will skip
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_w.htm#whitespace">whitespace</a>
|
||
characters, i.e. it will return the next non-whitespace character that’s waiting
|
||
on the stream. The whitespace characters will vanish from the stream as if they
|
||
had been read by <code>read-char</code>:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (with-input-from-string (stream "I'm not amused")
|
||
(print (read-char stream))
|
||
(print (read-char stream))
|
||
(print (read-char stream))
|
||
(print (peek-char t stream))
|
||
(print (read-char stream))
|
||
(print (read-char stream))
|
||
(values))
|
||
|
||
#\I
|
||
#\'
|
||
#\m
|
||
#\n
|
||
#\n
|
||
#\o
|
||
</code></pre>
|
||
|
||
<p>If the first argument to <code>peek-char</code> is a character, the function will skip all
|
||
characters until that particular character is found:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (with-input-from-string (stream "I'm not amused")
|
||
(print (read-char stream))
|
||
(print (peek-char #\a stream))
|
||
(print (read-char stream))
|
||
(print (read-char stream))
|
||
(values))
|
||
|
||
#\I
|
||
#\a
|
||
#\a
|
||
#\m
|
||
</code></pre>
|
||
|
||
<p>Note that <code>peek-char</code> has further optional arguments to control its behaviour on
|
||
end-of-file similar to those for <code>read-line</code> and <code>read-char</code> (and it will signal an
|
||
error by default):</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (with-input-from-string (stream "I'm not amused")
|
||
(print (read-char stream))
|
||
(print (peek-char #\d stream))
|
||
(print (read-char stream))
|
||
(print (peek-char nil stream nil 'the-end))
|
||
(values))
|
||
|
||
#\I
|
||
#\d
|
||
#\d
|
||
THE-END
|
||
</code></pre>
|
||
|
||
<p>You can also put one character back onto the stream with the function
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_unrd_c.htm"><code>unread-char</code></a>. You
|
||
can use it as if, <em>after</em> you have read a character, you decide that you’d
|
||
better used <code>peek-char</code> instead of <code>read-char</code>:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (with-input-from-string (stream "I'm not amused")
|
||
(let ((c (read-char stream)))
|
||
(print c)
|
||
(unread-char c stream)
|
||
(print (read-char stream))
|
||
(values)))
|
||
|
||
#\I
|
||
#\I
|
||
</code></pre>
|
||
|
||
<p>Note that the front of a stream doesn’t behave like a stack: You can only put
|
||
back exactly <em>one</em> character onto the stream. Also, you <em>must</em> put back the same
|
||
character that has been read previously, and you can’t unread a character if
|
||
none has been read before.</p>
|
||
|
||
<h4 id="random-access-to-a-file">Random access to a File</h4>
|
||
|
||
<p>Use the function
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_file_p.htm"><code>file-position</code></a>
|
||
for random access to a file. If this function is used with one argument (a
|
||
stream), it will return the current position within the stream. If it’s used
|
||
with two arguments (see below), it will actually change the
|
||
<a href="http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_f.htm#file_position">file position</a>
|
||
in the stream.</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (with-input-from-string (stream "I'm not amused")
|
||
(print (file-position stream))
|
||
(print (read-char stream))
|
||
(print (file-position stream))
|
||
(file-position stream 4)
|
||
(print (file-position stream))
|
||
(print (read-char stream))
|
||
(print (file-position stream))
|
||
(values))
|
||
|
||
0
|
||
#\I
|
||
1
|
||
4
|
||
#\n
|
||
5
|
||
</code></pre>
|
||
|
||
<h3 id="writing-content-to-a-file">Writing content to a file</h3>
|
||
|
||
<p>With <code>with-open-file</code>, specify <code>:direction :output</code> and use <code>write-sequence</code> inside:</p>
|
||
|
||
<pre><code class="language-lisp">(with-open-file (f <pathname> :direction :output
|
||
:if-exists :supersede
|
||
:if-does-not-exist :create)
|
||
(write-sequence s f))
|
||
</code></pre>
|
||
|
||
<p>If the file exists, you can also <code>:append</code> content to it.</p>
|
||
|
||
<p>If it doesn’t exist, you can <code>:error</code> out. See <a href="http://www.lispworks.com/documentation/HyperSpec/Body/f_open.htm">the standard</a> for more details.</p>
|
||
|
||
<h4 id="using-libraries">Using libraries</h4>
|
||
|
||
<p>The library <a href="https://common-lisp.net/project/alexandria/draft/alexandria.html#Conses">Alexandria</a>
|
||
has a function called <a href="https://gitlab.common-lisp.net/alexandria/alexandria/-/blob/master/alexandria-1/io.lisp#L73">write-string-into-file</a></p>
|
||
|
||
<pre><code class="language-lisp">(alexandria:write-string-into-file content "file.txt")
|
||
</code></pre>
|
||
|
||
<p>Alternatively, the library <a href="https://github.com/vindarel/cl-str">str</a> has the <code>to-file</code> function.</p>
|
||
|
||
<pre><code class="language-lisp">(str:to-file "file.txt" content) ;; with optional options
|
||
</code></pre>
|
||
|
||
<p>Both <code>alexandria:write-string-into-file</code> and <code>str:to-file</code> take the same keyword arguments as <code>cl:open</code> that controls file creation: <code>:if-exists</code> and <code>if-does-not-exists</code>.</p>
|
||
|
||
<h3 id="getting-file-attributes-size-access-time">Getting file attributes (size, access time,…)</h3>
|
||
|
||
<p><a href="https://www.common-lisp.net/project/osicat/">Osicat</a>
|
||
is a lightweight operating system interface for Common Lisp on
|
||
POSIX-like systems, including Windows. With Osicat we can get and set
|
||
<strong>environment variables</strong> (now doable with <code>uiop:getenv</code>),
|
||
manipulate <strong>files and directories</strong>,
|
||
<strong>pathnames</strong> and a bit more.</p>
|
||
|
||
<p><a href="https://github.com/Shinmera/file-attributes/">file-attributes</a> is a
|
||
newer and lighter OS portability library specifically for getting file attributes,
|
||
using system calls (cffi).</p>
|
||
|
||
<p>SBCL with its <code>sb-posix</code> contrib can be used too.</p>
|
||
|
||
<h4 id="file-attributes-osicat">File attributes (Osicat)</h4>
|
||
|
||
<p>Once Osicat is installed, it also defines the <code>osicat-posix</code> system,
|
||
which permits us to get file attributes.</p>
|
||
|
||
<pre><code class="language-lisp">(ql:quickload "osicat")
|
||
|
||
(let ((stat (osicat-posix:stat #P"./files.md")))
|
||
(osicat-posix:stat-size stat)) ;; => 10629
|
||
</code></pre>
|
||
|
||
<p>We can get the other attributes with the following methods:</p>
|
||
|
||
<pre><code>osicat-posix:stat-dev
|
||
osicat-posix:stat-gid
|
||
osicat-posix:stat-ino
|
||
osicat-posix:stat-uid
|
||
osicat-posix:stat-mode
|
||
osicat-posix:stat-rdev
|
||
osicat-posix:stat-size
|
||
osicat-posix:stat-atime
|
||
osicat-posix:stat-ctime
|
||
osicat-posix:stat-mtime
|
||
osicat-posix:stat-nlink
|
||
osicat-posix:stat-blocks
|
||
osicat-posix:stat-blksize
|
||
</code></pre>
|
||
|
||
<h4 id="file-attributes-file-attributes">File attributes (file-attributes)</h4>
|
||
|
||
<p>Install the library with</p>
|
||
|
||
<pre><code>(ql:quickload "file-attributes")
|
||
</code></pre>
|
||
|
||
<p>Its package is <code>org.shirakumo.file-attributes</code>. You can use a
|
||
package-local nickname for a shorter access to its functions, for example:</p>
|
||
|
||
<pre><code class="language-lisp">(uiop:add-package-local-nickname :file-attributes :org.shirakumo.file-attributes)
|
||
</code></pre>
|
||
|
||
<p>Then simply use the functions:</p>
|
||
|
||
<ul>
|
||
<li><code>access-time</code>, <code>modification-time</code>, <code>creation-time</code>. You can <code>setf</code> them.</li>
|
||
<li><code>owner</code>, <code>group</code>, and <code>attributes</code>. The values used are OS specific
|
||
for these functions. The attributes flag can be decoded and
|
||
encoded via a standardised form with <code>decode-attributes</code> and
|
||
<code>encode-attributes</code>.</li>
|
||
</ul>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (file-attributes:decode-attributes
|
||
(file-attributes:attributes #p"test.txt"))
|
||
(:READ-ONLY NIL :HIDDEN NIL :SYSTEM-FILE NIL :DIRECTORY NIL :ARCHIVED T :DEVICE
|
||
NIL :NORMAL NIL :TEMPORARY NIL :SPARSE NIL :LINK NIL :COMPRESSED NIL :OFFLINE
|
||
NIL :NOT-INDEXED NIL :ENCRYPTED NIL :INTEGRITY NIL :VIRTUAL NIL :NO-SCRUB NIL
|
||
:RECALL NIL)
|
||
</code></pre>
|
||
|
||
<p>See <a href="https://shinmera.github.io/file-attributes">its documentation</a>.</p>
|
||
|
||
<h4 id="file-attributes-sb-posix">File attributes (sb-posix)</h4>
|
||
|
||
<p>This contrib is loaded by default on POSIX systems.</p>
|
||
|
||
<p>First get a stat object for a file, then get the stat you want:</p>
|
||
|
||
<pre><code class="language-lisp">CL-USER> (sb-posix:stat "test.txt")
|
||
#<SB-POSIX:STAT {10053FCBE3}>
|
||
|
||
CL-USER> (sb-posix:stat-mtime *)
|
||
1686671405
|
||
</code></pre>
|
||
|
||
<h3 id="listing-files-and-directories">Listing files and directories</h3>
|
||
|
||
<p>Some functions below return pathnames, so you might need the following:</p>
|
||
|
||
<pre><code class="language-lisp">(namestring #p"/foo/bar/baz.txt") ==> "/foo/bar/baz.txt"
|
||
(directory-namestring #p"/foo/bar/baz.txt") ==> "/foo/bar/"
|
||
(file-namestring #p"/foo/bar/baz.txt") ==> "baz.txt"
|
||
</code></pre>
|
||
|
||
<h4 id="listing-files-in-a-directory">Listing files in a directory</h4>
|
||
|
||
<pre><code class="language-lisp">(uiop:directory-files "./")
|
||
</code></pre>
|
||
|
||
<p>Returns a list of pathnames:</p>
|
||
|
||
<pre><code>(#P"/home/vince/projects/cl-cookbook/.emacs"
|
||
#P"/home/vince/projects/cl-cookbook/.gitignore"
|
||
#P"/home/vince/projects/cl-cookbook/AppendixA.jpg"
|
||
#P"/home/vince/projects/cl-cookbook/AppendixB.jpg"
|
||
#P"/home/vince/projects/cl-cookbook/AppendixC.jpg"
|
||
#P"/home/vince/projects/cl-cookbook/CHANGELOG"
|
||
#P"/home/vince/projects/cl-cookbook/CONTRIBUTING.md"
|
||
[…]
|
||
</code></pre>
|
||
|
||
<h4 id="listing-sub-directories">Listing sub-directories</h4>
|
||
|
||
<pre><code class="language-lisp">(uiop:subdirectories "./")
|
||
</code></pre>
|
||
|
||
<pre><code>(#P"/home/vince/projects/cl-cookbook/.git/"
|
||
#P"/home/vince/projects/cl-cookbook/.sass-cache/"
|
||
#P"/home/vince/projects/cl-cookbook/_includes/"
|
||
#P"/home/vince/projects/cl-cookbook/_layouts/"
|
||
#P"/home/vince/projects/cl-cookbook/_site/"
|
||
#P"/home/vince/projects/cl-cookbook/assets/")
|
||
</code></pre>
|
||
|
||
<h4 id="iterating-on-files-lazily">Iterating on files (lazily)</h4>
|
||
|
||
<p>In addition to the above functions, we mention solutions that <em>lazily</em>
|
||
traverse a directory. They don’t load the entire list of files before
|
||
returning it.</p>
|
||
|
||
<p>Osicat has <code>with-directory-iterator</code>:</p>
|
||
|
||
<pre><code class="language-lisp">(with-directory-iterator (next "/")
|
||
(loop for entry = (next)
|
||
while entry
|
||
when (member :group-write (file-permissions entry))
|
||
collect entry))
|
||
;; => (#P"tmp/")
|
||
</code></pre>
|
||
|
||
<p>LispWorks has the <a href="https://www.lispworks.com/documentation/lw80/lw/lw-hcl-74.htm#LWUGRM">fast-directory-files</a> function, and AllegroCL has <a href="https://franz.com/support/documentation/10.1/doc/operators/excl/map-over-directory.htm">map-over-directory</a>.</p>
|
||
|
||
<h4 id="traversing-walking-directories-recursively">Traversing (walking) directories recursively</h4>
|
||
|
||
<p>See <code>uiop/filesystem:collect-sub*directories</code>. It takes as arguments:</p>
|
||
|
||
<ul>
|
||
<li>a <code>directory</code></li>
|
||
<li>a <code>collectp</code> function</li>
|
||
<li>a <code>recursep</code> function</li>
|
||
<li>a <code>collector</code> function</li>
|
||
</ul>
|
||
|
||
<p>Given a directory, when <code>collectp</code> returns true with the directory,
|
||
call the <code>collector</code> function on the directory, and recurse
|
||
each of its subdirectories on which <code>recursep</code> returns true.</p>
|
||
|
||
<p>This function will thus let you traverse a filesystem hierarchy,
|
||
superseding the functionality of <code>cl-fad:walk-directory</code>.</p>
|
||
|
||
<p>The behavior in presence of symlinks is not portable. Use IOlib to handle such situations.</p>
|
||
|
||
<p>Examples:</p>
|
||
|
||
<ul>
|
||
<li>this collects only subdirectories:</li>
|
||
</ul>
|
||
|
||
<pre><code class="language-lisp">(defparameter *dirs* nil "All recursive directories.")
|
||
|
||
(uiop:collect-sub*directories "~/cl-cookbook"
|
||
(constantly t)
|
||
(constantly t)
|
||
(lambda (it) (push it *dirs*)))
|
||
</code></pre>
|
||
|
||
<ul>
|
||
<li>this collects files and subdirectories:</li>
|
||
</ul>
|
||
|
||
<pre><code class="language-lisp">(let ((results))
|
||
(uiop:collect-sub*directories
|
||
"./"
|
||
(constantly t)
|
||
(constantly t)
|
||
(lambda (subdir)
|
||
(setf results
|
||
(nconc results
|
||
;; A detail: we return strings, not pathnames.
|
||
(loop for path in (append (uiop:subdirectories subdir)
|
||
(uiop:directory-files subdir))
|
||
collect (namestring path))))))
|
||
results)
|
||
</code></pre>
|
||
|
||
<ul>
|
||
<li>we can do the same with the <code>cl-fad</code> library:</li>
|
||
</ul>
|
||
|
||
<pre><code class="language-lisp">(cl-fad:walk-directory "./"
|
||
(lambda (name)
|
||
(format t "~A~%" name))
|
||
:directories t)
|
||
</code></pre>
|
||
|
||
<ul>
|
||
<li>and of course, we can use an external tool: the good ol’ unix <code>find</code>, or the newer <code>fd</code> (<code>fdfind</code> on Debian) that has a simpler syntax and filters out a set of common files and directories by default (node_modules, .git…):</li>
|
||
</ul>
|
||
|
||
<pre><code class="language-lisp">(str:lines (uiop:run-program (list "find" ".") :output :string))
|
||
;; or
|
||
(str:lines (uiop:run-program (list "fdfind") :output :string))
|
||
</code></pre>
|
||
|
||
<p>Here with the help of the <code>str</code> library.</p>
|
||
|
||
<h4 id="finding-files-matching-a-pattern">Finding files matching a pattern</h4>
|
||
|
||
<p>Below we simply list files of a directory and check that their name
|
||
contains a given string.</p>
|
||
|
||
<pre><code class="language-lisp">(remove-if-not (lambda (it)
|
||
(search "App" (namestring it)))
|
||
(uiop:directory-files "./"))
|
||
</code></pre>
|
||
|
||
<pre><code>(#P"/home/vince/projects/cl-cookbook/AppendixA.jpg"
|
||
#P"/home/vince/projects/cl-cookbook/AppendixB.jpg"
|
||
#P"/home/vince/projects/cl-cookbook/AppendixC.jpg")
|
||
</code></pre>
|
||
|
||
<p>We used <code>namestring</code> to convert a <code>pathname</code> to a string, thus a
|
||
sequence that <code>search</code> can deal with.</p>
|
||
|
||
<h4 id="finding-files-with-a-wildcard">Finding files with a wildcard</h4>
|
||
|
||
<p>We can not transpose unix wildcards to portable Common Lisp.</p>
|
||
|
||
<p>In pathname strings we can use <code>*</code> and <code>**</code> as wildcards. This works
|
||
in absolute and relative pathnames.</p>
|
||
|
||
<pre><code class="language-lisp">(directory #P"*.jpg")
|
||
</code></pre>
|
||
|
||
<pre><code class="language-lisp">(directory #P"**/*.png")
|
||
</code></pre>
|
||
|
||
<h4 id="change-the-default-pathname">Change the default pathname</h4>
|
||
|
||
<p>The concept of <code>.</code> denoting the current directory does not exist in
|
||
portable Common Lisp. This may exist in specific filesystems and
|
||
specific implementations.</p>
|
||
|
||
<p>Also <code>~</code> to denote the home directory does not exist. They may be
|
||
recognized by some implementations as non-portable extensions.</p>
|
||
|
||
<p><code>*default-pathname-defaults*</code>provides a default for some pathname
|
||
operations.</p>
|
||
|
||
<pre><code class="language-lisp">(let ((*default-pathname-defaults* (pathname "/bin/")))
|
||
(directory "*sh"))
|
||
(#P"/bin/zsh" #P"/bin/tcsh" #P"/bin/sh" #P"/bin/ksh" #P"/bin/csh" #P"/bin/bash")
|
||
</code></pre>
|
||
|
||
<p>See also <code>(user-homedir-pathname)</code>.</p>
|
||
|
||
|
||
|
||
<p class="page-source">
|
||
Page source: <a href="https://github.com/LispCookbook/cl-cookbook/blob/master/files.md">files.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">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>
|