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

904 lines
35 KiB
HTML
Raw Normal View History

2023-10-25 11:23:21 +02:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="generator" content=
"HTML Tidy for HTML5 for Linux version 5.2.0">
<title>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> &ndash; 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> &ndash; 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">
2024-01-12 09:23:31 +01:00
📢 🤶 ⭐
<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>
2023-10-25 11:23:21 +02:00
<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>Well 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") ;; =&gt; "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") ;; =&gt; "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") ;; =&gt; "foo"
(pathname-name "~/foo") ;; =&gt; "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/") ;; =&gt; NIL
(first (last (pathname-directory #P"~/foo/"))) ;; =&gt; "foo"
</code></pre>
<h4 id="parent-directory">Parent directory</h4>
2024-01-12 09:23:31 +01:00
<pre><code class="language-lisp">(uiop:pathname-parent-directory-pathname #P"/foo/bar/quux/")
;; =&gt; #P"/foo/bar/"
2023-10-25 11:23:21 +02:00
</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 doesnt 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 dont 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
doesnt exist, <code>namestring</code> doesnt 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 doesnt 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&gt; (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>. Dont 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, heres how to append a directory to another one:</p>
<pre><code class="language-lisp">(merge-pathnames "otherpath" "/home/vince/projects/")
2024-01-12 09:23:31 +01:00
;; important: ^^
;; a trailing / denotes a directory.
2023-10-25 11:23:21 +02:00
;; =&gt; #P"/home/vince/projects/otherpath"
</code></pre>
<p>Look at the difference: if you dont 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/")
;; =&gt; #P"/home/vince/projects/mysystem/src/web/"
</code></pre>
<p>This will work on another users 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 youre 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 youre
done, itll 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 &lt;_file-spec_&gt;
:direction &lt;_direction_&gt;
:if-exists &lt;_if-exists_&gt;
:if-does-not-exist &lt;_if-does-not-exist_&gt;)
(your code here))
</code></pre>
<ul>
<li><code>str</code> is a variable whichll be bound to the stream which is created by
opening the file.</li>
<li><code>&lt;_file-spec_&gt;</code> will be a truename or a pathname.</li>
<li><code>&lt;_direction_&gt;</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>&lt;_if-exists_&gt;</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>&lt;_if-does-not-exist_&gt;</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. Youll find some examples on how to use <code>with-open-file</code>
below. Also note that you usually dont 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>Its 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 wont 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 (&lt; 0 n-characters)
do (write-sequence buffer out :start 0 :end n-characters)))))
</code></pre>
<p>Furthermore, youre 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 SBCLs default encoding format to utf-8</h4>
<p>Sometimes you dont control the internals of a library, so youd
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 arent 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 thats
waiting on the stream:</p>
<pre><code class="language-lisp">CL-USER&gt; (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 thats 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&gt; (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&gt; (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&gt; (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 youd
better used <code>peek-char</code> instead of <code>read-char</code>:</p>
<pre><code class="language-lisp">CL-USER&gt; (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 doesnt 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 cant 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 its 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&gt; (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 &lt;pathname&gt; :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 doesnt 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)) ;; =&gt; 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&gt; (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&gt; (sb-posix:stat "test.txt")
#&lt;SB-POSIX:STAT {10053FCBE3}&gt;
CL-USER&gt; (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") ==&gt; "/foo/bar/baz.txt"
(directory-namestring #p"/foo/bar/baz.txt") ==&gt; "/foo/bar/"
(file-namestring #p"/foo/bar/baz.txt") ==&gt; "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="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/>
&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">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>