about summary refs log tree commit diff
path: root/third_party/lisp/sclf/directory.lisp
blob: 3e479c4ac279583fb9dac23c61d8df32c3956cd5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
;;;  directory.lisp --- filesystem directory access

;;;  Copyright (C) 2006, 2007, 2008, 2009, 2010 by Walter C. Pelissero
;;;  Copyright (C) 2021 by the TVL Authors

;;;  Author: Walter C. Pelissero <walter@pelissero.de>
;;;  Project: sclf

#+cmu (ext:file-comment "$Module: directory.lisp $")

;;; This library is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU Lesser General Public License
;;; as published by the Free Software Foundation; either version 2.1
;;; of the License, or (at your option) any later version.
;;; This library is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;;; Lesser General Public License for more details.
;;; You should have received a copy of the GNU Lesser General Public
;;; License along with this library; if not, write to the Free
;;; Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
;;; 02111-1307 USA


(cl:in-package :sclf)

(defun pathname-as-directory (pathname)
  "Converts PATHNAME to directory form and return it."
  (setf pathname (pathname pathname))
  (if (pathname-name pathname)
      (make-pathname :directory (append (or (pathname-directory pathname)
                                            '(:relative))
                                        (list (file-namestring pathname)))
                     :name nil
                     :type nil
                     :defaults pathname)
      pathname))

(defun d+ (path &rest rest)
  "Concatenate directory pathname parts and return a pathname."
  (make-pathname :defaults path
                 :directory (append (pathname-directory path) rest)))

(defun delete-directory (pathname)
  "Remove directory PATHNAME.  Return PATHNAME."
  #+cmu (multiple-value-bind (done errno)
             (unix:unix-rmdir (namestring pathname))
           (unless done
             (error "Unable to delete directory ~A (errno=~A)"
                    pathname errno)))
  #+sbcl (sb-posix:rmdir pathname)
  #+lispworks (lw:delete-directory pathname)
  #-(or cmu sbcl)
  (error "DELETE-DIRECTORY not implemented for you lisp system.")
  pathname)

(defun list-directory (pathname &key truenamep)
  "List content of directory PATHNAME.  If TRUENAMEP is true don't try
to follow symbolic links."
  #-(or sbcl cmu) (declare (ignore truenamep))
  (let (#+cmu (lisp::*ignore-wildcards* t))
    (directory (make-pathname :defaults (pathname-as-directory pathname)
                              :name :wild
                              :type :wild
                              :version :wild)
               #+cmu :truenamep #+cmu truenamep
               #+sbcl :resolve-symlinks #+sbcl truenamep)))

(defun traverse-directory-tree (root-pathname proc &key truenamep test depth-first)
  "Call PROC on all pathnames under ROOT-PATHNAME, both files and
directories.  Unless TRUENAMEP is true, this function doesn't try
to lookup the truename of files, as finding the truename may be a
superfluous and noxious activity expecially when you expect
broken symbolic links in your filesystem."
  (check-type root-pathname pathname)
  (check-type proc (or function symbol))
  (check-type test (or function symbol null))
  (labels ((ls (dir)
             (declare (type pathname dir))
             (list-directory dir :truenamep truenamep))
           (traverse? (file)
             (declare (type pathname file))
             (and (not (pathname-name file))
                  (or truenamep
                      (not (symbolic-link-p file)))
                  (or (not test)
                      (funcall test file))))
           (traverse-pre-order (dir)
             (declare (type pathname dir))
             (loop
                for file in (ls dir)
                do (funcall proc file)
                when (traverse? file)
                do (traverse-pre-order file)))
           (traverse-post-order (dir)
             (declare (type pathname dir))
             (loop
                for file in (ls dir)
                when (traverse? file)
                do (traverse-post-order file)
                do (funcall proc file))))
    (if depth-first
        (traverse-post-order root-pathname)
        (traverse-pre-order root-pathname))
    (values)))

(defmacro do-directory-tree ((file root-pathname &key truenamep test depth-first) &body body)
  "Call TRAVERSE-DIRECTORY-TREE with BODY es procedure."
  `(traverse-directory-tree ,root-pathname
                            #'(lambda (,file)
                                ,@body)
                            :truenamep ,truenamep
                            :test ,test
                            :depth-first ,depth-first))

(defun empty-directory-p (pathname)
  (and (directory-p pathname)
       (endp (list-directory pathname))))

(defun remove-empty-directories (root)
  (do-directory-tree (pathname root :depth-first t)
    (when (empty-directory-p pathname)
      (delete-directory pathname))))

(defun map-directory-tree (pathname function)
  "Apply FUNCTION to every file in a directory tree starting from
PATHNAME.  Return the list of results."
  (be return-list '()
    (do-directory-tree (directory-entry pathname)
      (push (funcall function directory-entry) return-list))
    (nreverse return-list)))

(defun find-files (root-pathname matcher-function &key truenamep)
  "In the directory tree rooted at ROOT-PATHNAME, find files that
when the pathname is applied to MATCHER-FUNCTION will return
true.  Return the list of files found.  Unless TRUENAMEP is true
this function doesn't try to lookup the truename of
files. Finding the truename may be a superfluous and noxious
activity expecially when you expect broken symbolic links in your
filesystem.  (This may not apply to your particular lisp
system.)"
  (be files '()
    (do-directory-tree (file root-pathname :truenamep truenamep)
      (when (funcall matcher-function file)
        (push file files)))
    (nreverse files)))

(defun delete-directory-tree (pathname)
  "Recursively delete PATHNAME and all the directory structure below
it.

WARNING: depending on the way the DIRECTORY function is implemented on
your Lisp system this function may follow Unix symbolic links and thus
delete files outside the PATHNAME hierarchy.  Check this before using
this function in your programs."
  (if (pathname-name pathname)
      (delete-file pathname)
      (progn
        (dolist (file (list-directory pathname))
          (delete-directory-tree file))
        (delete-directory pathname))))

(defun make-directory (pathname &optional (mode #o777))
  "Create a new directory in the filesystem.  Permissions MODE
will be assigned to it.  Return PATHNAME."
  #+cmu (multiple-value-bind (done errno)
            (unix:unix-mkdir (native-namestring pathname) mode)
          (unless done
            (error "Unable to create directory ~A (errno=~A)." pathname errno)))
  #+sbcl (sb-posix:mkdir pathname mode)
  #-(or cmu sbcl)
  (error "MAKE-DIRECTORY is not implemented for this Lisp system.")
  pathname)

;; At least on SBCL/CMUCL + Unix + NFS this function is faster than
;; ENSURE-DIRECTORIES-EXIST, because it doesn't check all the pathname
;; components starting from the root; it proceeds from the leaf and
;; crawls the directory tree upward only if necessary."
(defun ensure-directory (pathname &key verbose (mode #o777))
  "Just like ENSURE-DIRECTORIES-EXIST but, in some situations,
it's faster."
  (labels ((ensure (path)
             (unless (probe-file path)
               (be* tail (last (pathname-directory path) 2)
                    last (cdr tail)
                 (setf (cdr tail) nil)
                 (unwind-protect
                      (ensure path)
                   (setf (cdr tail) last))
                 (make-directory path mode)
                 (when verbose
                   (format t "Created ~S~%" path))))))
    (ensure (make-pathname :defaults pathname
                           :name nil :type nil
                           :version nil))))

(defun make-temp-directory (&optional (default-pathname *tmp-file-defaults*) (mode #o777))
  "Create a new directory and return its pathname.
If DEFAULT-PATHNAME is specified and not NIL it's used as
defaults to produce the pathname of the directory.  Return the
pathname of the temporary directory."
  (loop
     for name = (pathname-as-directory (temp-file-name default-pathname))
     when (ignore-errors (make-directory name mode))
     return name))

(defmacro with-temp-directory ((path &rest make-temp-directory-args) &body body)
  "Execute BODY with PATH bound to the pathname of a new unique
temporary directory.  On exit of BODY the directory tree starting from
PATH will be automatically removed from the filesystem.  Return what
BODY returns.  BODY is _not_ executed within the PATH directory; the
working directory is never changed."
  `(be ,path (make-temp-directory ,@make-temp-directory-args)
     (unwind-protect
          (progn ,@body)
       (delete-directory-tree ,path))))

(defun current-directory ()
  "Return the pathname of the current directory."
  (truename (make-pathname :directory '(:relative))))

(defun ensure-home-translations ()
  "Ensure that the logical pathname translations for the host \"home\"
are defined."
  ;; CMUCL already defines a HOME translation of its own and gets
  ;; angry if we try to redefine it
  #-cmu
  (be home (user-homedir-pathname)
    ;; we should discard and replace whatever has been defined in any
    ;; rc file during compilation
    (setf (logical-pathname-translations "home")
          (list
           (list "**;*.*.*"
                 (make-pathname :defaults home
                                :directory (append (pathname-directory home)
                                                   '(:wild-inferiors))
                                :name :wild
                                :type :wild))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun parse-native-namestring (string &optional host (defaults *default-pathname-defaults*)
                                &key (start 0) end junk-allowed)
  #+sbcl (sb-ext:parse-native-namestring string host defaults
                                         :start start
                                         :end end
                                         :junk-allowed junk-allowed)
  #-sbcl (let (#+cmu(lisp::*ignore-wildcards* t))
           (parse-namestring string host defaults
                             :start start
                             :end end
                             :junk-allowed junk-allowed)))

(defun native-namestring (pathname)
  #+sbcl (sb-ext:native-namestring pathname)
  #-sbcl (let (#+cmu (lisp::*ignore-wildcards* t))
           (namestring pathname)))

(defun native-file-namestring (pathname)
  #+sbcl (sb-ext:native-namestring
          (make-pathname :name (pathname-name pathname)
                         :type (pathname-type pathname)))
  #+cmu (be lisp::*ignore-wildcards* t
          (file-namestring pathname)))

(defun native-pathname (thing)
  #+sbcl (sb-ext:native-pathname thing)
  #+cmu (be lisp::*ignore-wildcards* t
          (pathname thing)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defun bits-set-p (x bits)
  (= (logand x bits)
     bits))

(defun directory-p (pathname)
  "Return true if PATHNAME names a directory on the filesystem."
  #-clisp (awhen (unix-stat (native-namestring pathname))
            (bits-set-p (stat-mode it)
                        #+sbcl sb-posix:s-ifdir
                        #+cmu unix:s-ifdir))
  #+clisp (ext:probe-directory (pathname-as-directory pathname)))

(defun regular-file-p (pathname)
  "Return true if PATHNAME names a regular file on the filesystem."
  #-(or sbcl cmu) (error "don't know how to check whether a file might be a regular file")
  (awhen (unix-stat (native-namestring pathname))
    (bits-set-p (stat-mode it)
                #+sbcl sb-posix:s-ifreg
                #+cmu unix:s-ifreg)))

(defun file-readable-p (pathname)
  #+sbcl (sb-unix:unix-access (native-namestring pathname) sb-unix:r_ok)
  #+cmu (unix:unix-access (native-namestring pathname) unix:r_ok)
  #-(or sbcl cmu) (error "don't know how to check whether a file might be readable"))

(defun file-writable-p (pathname)
  #+sbcl (sb-unix:unix-access (native-namestring pathname) sb-unix:w_ok)
  #+cmu (unix:unix-access (native-namestring pathname) unix:w_ok)
  #-(or sbcl cmu) (error "don't know how to check whether a file might be writable"))

(defun file-executable-p (pathname)
  #+sbcl (sb-unix:unix-access (native-namestring pathname) sb-unix:x_ok)
  #+cmu (unix:unix-access (native-namestring pathname) unix:x_ok)
  #-(or sbcl cmu) (error "don't know how to check whether a file might be executable"))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defstruct (unix-file-stat (:conc-name stat-))
  device
  inode
  links
  atime
  mtime
  ctime
  size
  blksize
  blocks
  uid
  gid
  mode)

(defun unix-stat (pathname)
  ;; this could be different depending on the unix systems
  (multiple-value-bind (ok? device inode mode links uid gid rdev
                            size atime mtime ctime
                            blksize blocks)
      (#+cmu unix:unix-lstat
       #+sbcl sb-unix:unix-lstat
       (if (stringp pathname)
           pathname
           (native-namestring pathname)))
    (declare (ignore rdev))
    (when ok?
      (make-unix-file-stat :device device
                           :inode inode
                           :links links
                           :atime atime
                           :mtime mtime
                           :ctime ctime
                           :size size
                           :blksize blksize
                           :blocks blocks
                           :uid uid
                           :gid gid
                           :mode mode))))

(defun stat-modification-time (stat)
  "Return the modification time of the STAT structure as Lisp
Universal Time, which is not the same as the Unix time."
  (unix->universal-time (stat-mtime stat)))

(defun stat-creation-time (stat)
  "Return the creation time of the STAT structure as Lisp
Universal Time, which is not the same as the Unix time."
  (unix->universal-time (stat-ctime stat)))

(defun file-modification-time (file)
  "Return the modification time of FILE as Lisp Universal Time, which
is not the same as the Unix time."
  (awhen (unix-stat file)
    (stat-modification-time it)))

(defun file-creation-time (file)
  "Return the creation time of FILE as Lisp Universal Time, which
is not the same as the Unix time."
  (awhen (unix-stat file)
    (stat-creation-time it)))

(defun read-symbolic-link (symlink)
  "Return the pathname the SYMLINK points to.  That is, it's
contents."
  #+sbcl (sb-posix:readlink (native-namestring symlink))
  #+cmu (unix:unix-readlink (native-namestring symlink)))

;; FILE-LENGTH is a bit idiosyncratic in this respect.  Besides, Unix
;; allows to get to know the file size without being able to open a
;; file; just ask politely.
(defun file-size (pathname)
  (stat-size (unix-stat pathname)))

(defun symbolic-link-p (pathname)
  #-(or sbcl cmu) (error "don't know hot to test for symbolic links.")
  (aand (unix-stat pathname)
        (bits-set-p (stat-mode it)
                    #+sbcl sb-posix:s-iflnk
                    #+cmu unix:s-iflnk)))

(defun broken-link-p (pathname)
 (when (symbolic-link-p pathname)
   #+cmu (not (ignore-errors (truename pathname)))
   ;; On a broken symlink SBCL returns the link path without resolving
   ;; the link itself.  De gustibus non est disputandum.
   #+sbcl (equalp pathname (probe-file pathname))))

(defun move-file (old new)
  "Just like RENAME-FILE, but doesn't carry on to NEW file the type of
OLD file, if NEW doesn't specify one.  It does what most people would
expect from a rename function, which RENAME-FILE doesn't do.
So (MOVE-FILE \"foo.bar\" \"foo\") does rename foo.bar to foo, losing
the \"bar\" type; RENAME-FILE wouldn't allow you that."
  #+sbcl (sb-posix:rename (native-namestring old) (native-namestring new))
  #+cmu (unix:unix-rename (native-namestring old) (native-namestring new)))