Coverage report: /home/ellis/comp/core/std/file.lisp
Kind | Covered | All | % |
expression | 31 | 551 | 5.6 |
branch | 0 | 60 | 0.0 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; std/file.lisp --- Standard File Library
8
(define-condition unknown-file-type (file-error) ()
9
(:report (lambda (c s) (format s "unknown file type: ~A" (file-error-pathname c))))
10
(:documentation "Error signaled when the type of a file is unknown."))
12
(defun unknown-file-type (file)
13
"Signal an error of type UNKNOWN-FILE-TYPE."
14
(error 'unknown-file-type :pathname file))
16
(defgeneric file (self)
17
(:documentation "Return the file associated with SELF."))
18
(defgeneric (setf file) (new self)
19
(:documentation "Set the value of the file associated with SELF to NEW."))
20
(defgeneric dir (self)
21
(:documentation "Return the directory associated with SELF."))
22
(defgeneric (setf dir) (new self)
23
(:documentation "Set the value of the directory associated with SELF to NEW."))
25
(declaim (ftype (function (t) string) read-file))
26
(defun read-file (path)
27
(declare (optimize (speed 3)))
28
(with-output-to-string (s)
29
(write-file-into-stream path s)))
32
"Create an anonymous temporary file of the given size. Returns a file descriptor."
33
(let (done fd pathname)
36
(setf (values fd pathname) (sb-posix:mkstemp "/dev/shm/tmp.XXXXXXXX"))
37
(sb-posix:unlink pathname)
38
(sb-posix:ftruncate fd size)
40
(when (and fd (not done)) (sb-posix:close fd)))
43
(declaim (inline octet-vector=/unsafe))
44
(defun octet-vector=/unsafe (v1 v2 start1 end1 start2 end2)
45
(declare (optimize (speed 3)
48
(compilation-speed 0))
49
(type octet-vector v1 v2)
50
(type array-index start1 start2)
51
(type array-length end1 end2))
52
(and (= (- end1 start1)
54
(loop for i from start1 below end1
55
for j from start2 below end2
56
always (eql (aref v1 i) (aref v2 j)))))
58
(defun octet-vector= (v1 v2 &key (start1 0) end1
60
"Like `string=' for octet vectors."
61
(declare (octet-vector v1 v2)
62
(array-index start1 start2)
63
((or array-length null) end1 end2)
65
(let* ((len1 (length v1))
68
(end2 (or end2 len2)))
69
(assert (<= start1 end1 len1))
70
(assert (<= start2 end2 len2))
71
(octet-vector=/unsafe v1 v2 start1 end1 start2 end2)))
73
(defun file-size-in-octets (file)
74
"Return the file-size of FILE in octets."
75
(multiple-value-bind (path namestring)
77
(string (values (pathname file)
79
(pathname (values file
80
(sb-ext:native-namestring file))))
81
(declare (ignorable path namestring))
82
(sb-posix:stat-size (sb-posix:stat path))))
84
(define-constant si-prefixes
111
:documentation "List as SI prefixes: power of ten, long form, short form.")
113
(define-constant si-prefixes-base-1000
114
(loop for (pow long short) in si-prefixes
115
unless (and (not (zerop pow))
117
collect (list (truncate pow 3) long short))
119
:documentation "The SI prefixes as powers of 1000, with centi, deci, deca and hecto omitted.")
121
(define-constant iec-prefixes
130
:documentation "The IEC binary prefixes, as powers of 2.")
134
"Is SEQ a sequence of one element?"
137
(defmacro si-prefix-rec (n base prefixes)
138
(cond ((null prefixes) (error "No prefixes!"))
140
(destructuring-bind ((power long short)) prefixes
141
`(values ,long ,short ,(expt base power))))
144
(let* ((halfway (ceiling (length prefixes) 2))
145
(lo (subseq prefixes 0 halfway))
146
(hi (subseq prefixes halfway))
147
(split (* (expt base (caar hi)))))
149
(si-prefix-rec ,n ,base ,lo)
150
(si-prefix-rec ,n ,base ,hi))))))
152
(defun si-prefix (n &key (base 1000))
153
"Given a number, return the prefix of the nearest SI unit.
155
Three values are returned: the long form, the short form, and the
158
(si-prefix 1001) => \"kilo\", \"k\", 1000d0
160
BASE can be 1000, 10, 1024, or 2. 1000 is the default, and prefixes
161
start at kilo and milli. Base 10 is mostly the same, except the
162
prefixes centi, deci, deca and hecto are also used. Base 1024 uses the
163
same prefixes as 1000, but with 1024 as the base, as in vulgar file
164
sizes. Base 2 uses the IEC binary prefixes."
165
(if (zerop n) (values "" "" 1d0)
166
(let ((n (abs (coerce n 'double-float))))
168
(2 (si-prefix-rec n 2d0 #.iec-prefixes))
169
(10 (si-prefix-rec n 10d0 #.si-prefixes))
170
(1000 (si-prefix-rec n 1000d0 #.si-prefixes-base-1000))
171
(1024 (si-prefix-rec n 1024d0 #.si-prefixes-base-1000))))))
173
(defun human-size-formatter (size &key (flavor :si)
174
(space (eql flavor :si)))
175
"Auxiliary function for formatting quantities human-readably.
176
Returns two values: a format control and a list of arguments.
178
This can be used to integrate the human-readable printing of
179
quantities into larger format control strings using the recursive
180
processing format directive (~?):
182
(multiple-value-bind (control args)
183
(human-size-formatter size)
184
(format t \"~?\" control args))"
185
(let ((size (coerce size 'double-float))
186
;; Avoid printing exponent markers.
187
(*read-default-float-format* 'double-float)
192
(multiple-value-bind (long short factor)
193
(si-prefix size :base base)
194
(declare (ignore long))
195
(let* ((size (/ size factor))
198
(if (> (abs (- size int))
202
(values (formatter "~:[~d~;~,1f~]~:[~; ~]~a")
203
(list (floatp size) size space short))))))
205
(defun format-human-size (stream size
207
(space (eql flavor :si)))
208
"Write SIZE to STREAM, in human-readable form.
210
STREAM is interpreted as by `format'.
212
If FLAVOR is `:si' (the default) the base is 1000 and SI prefixes are used.
214
If FLAVOR is `:file', the base is 1024 and SI prefixes are used.
216
If FLAVOR is `:iec', the base is 1024 bytes and IEC prefixes (Ki, Mi,
219
If SPACE is non-nil, include a space between the number and the
220
prefix. (Defaults to T if FLAVOR is `:si'.)"
223
(multiple-value-bind (formatter args)
224
(human-size-formatter size :flavor flavor :space space)
225
(format stream "~?" formatter args))))
227
(defun format-file-size-human-readable (stream file-size
229
(space (eql flavor :si))
230
(suffix (if (eql flavor :iec) "B" "")))
231
"Write FILE-SIZE, a file size in bytes, to STREAM, in human-readable form.
233
STREAM is interpreted as by `format'.
235
If FLAVOR is nil, kilobytes are 1024 bytes and SI prefixes are used.
237
If FLAVOR is `:si', kilobytes are 1000 bytes and SI prefixes are used.
239
If FLAVOR is `:iec', kilobytes are 1024 bytes and IEC prefixes (Ki,
242
If SPACE is non-nil, include a space between the number and the
243
prefix. (Defaults to T if FLAVOR is `:si'.)
245
SUFFIX is the suffix to use; defaults to B if FLAVOR is `:iec',
247
(check-type file-size (integer 0 *))
248
(if (zerop file-size)
250
(let ((flavor (if (null flavor) :file flavor)))
251
(multiple-value-bind (formatter args)
252
(human-size-formatter file-size :flavor flavor :space space)
253
(format stream "~?~a" formatter args suffix)))))
255
(defun file-size-human-readable (file &key flavor space suffix stream)
256
"Format the size of FILE (in octets) using `format-file-size-human-readable'.
257
The size of file is found by `trivial-file-size:file-size-in-octets'.
259
Inspired by the function of the same name in Emacs."
260
(let ((file-size (file-size-in-octets file)))
261
(format-file-size-human-readable
268
(defmacro with-open-files ((&rest args) &body body)
269
"A simple macro to open one or more files providing the streams for the
270
BODY. The ARGS is a list of `(stream filespec options*)` as supplied to
276
`(with-open-file ,(first args) ,@body))
277
(t `(with-open-file ,(first args)
279
,(rest args) ,@body)))))
281
(defmacro with-open-file* ((stream filespec &key direction element-type
282
if-exists if-does-not-exist external-format)
284
"Just like WITH-OPEN-FILE, but NIL values in the keyword arguments
285
mean to use the default value specified for OPEN."
286
(once-only (direction element-type if-exists if-does-not-exist external-format)
288
(,stream (apply #'open ,filespec
291
(list :direction ,direction))
292
(list :element-type (or ,element-type
293
+default-element-type+))
295
(list :if-exists ,if-exists))
296
(when ,if-does-not-exist
297
(list :if-does-not-exist ,if-does-not-exist))
298
(when ,external-format
299
(list :external-format ,external-format)))))
302
(defmacro with-input-from-file ((stream-name file-name &rest args
303
&key (direction nil direction-p)
306
"Evaluate BODY with STREAM-NAME to an input stream on the file
307
FILE-NAME. ARGS is sent as is to the call to OPEN except EXTERNAL-FORMAT,
308
which is only sent to WITH-OPEN-FILE when it's not NIL."
309
(declare (ignore direction))
311
(error "Can't specify :DIRECTION for WITH-INPUT-FROM-FILE."))
312
`(with-open-file* (,stream-name ,file-name :direction :input ,@args)
315
(defmacro with-output-to-file ((stream-name file-name &rest args
316
&key (direction nil direction-p)
319
"Evaluate BODY with STREAM-NAME to an output stream on the file
320
FILE-NAME. ARGS is sent as is to the call to OPEN except EXTERNAL-FORMAT,
321
which is only sent to WITH-OPEN-FILE when it's not NIL."
322
(declare (ignore direction))
324
(error "Can't specify :DIRECTION for WITH-OUTPUT-TO-FILE."))
325
`(with-open-file* (,stream-name ,file-name :direction :output ,@args)
328
(defun write-stream-into-file (stream pathname &key (if-exists :error) if-does-not-exist)
329
"Read STREAM and write the contents into PATHNAME.
331
STREAM will be closed afterwards, so wrap it with
332
`make-concatenated-stream' if you want it left open."
333
(check-type pathname pathname)
334
(with-output-to-file (out pathname
335
:element-type (stream-element-type stream)
337
:if-does-not-exist if-does-not-exist)
338
(copy-stream stream out))
341
(defun write-file-into-stream (pathname output &key (if-does-not-exist :error)
342
(external-format :default))
343
"Write the contents of FILE into STREAM."
344
(check-type pathname pathname)
345
(with-input-from-file (input pathname
346
:element-type (stream-element-type output)
347
:if-does-not-exist if-does-not-exist
348
:external-format external-format)
349
(copy-stream input output :end (file-size-in-octets pathname))))
351
(defun file= (file1 file2 &key (buffer-size 4096))
352
"Compare FILE1 and FILE2 octet by octet, \(possibly) using buffers
354
(declare (ignorable buffer-size))
355
(let ((file1 (truename file1))
356
(file2 (truename file2)))
357
(or (equal file1 file2)
358
(and (= (file-size-in-octets file1)
359
(file-size-in-octets file2))
360
#+ccl (file=/mmap file1 file2)
361
#-ccl (file=/loop file1 file2 :buffer-size buffer-size)))))
363
(defun file=/loop (file1 file2 &key (buffer-size 4096))
364
"Compare two files by looping over their contents using a buffer."
366
(type pathname file1 file2)
367
(type array-length buffer-size)
368
(optimize (safety 1) (debug 0) (compilation-speed 0)))
369
(flet ((make-buffer ()
370
(make-array buffer-size
372
:initial-element 0)))
373
(declare (inline make-buffer))
374
(with-open-files ((file1 file1 :element-type 'octet :direction :input)
375
(file2 file2 :element-type 'octet :direction :input))
376
(and (= (file-length file1)
378
(locally (declare (optimize speed))
379
(loop with buffer1 = (make-buffer)
380
with buffer2 = (make-buffer)
381
for end1 = (read-sequence buffer1 file1)
382
for end2 = (read-sequence buffer2 file2)
383
until (or (zerop end1) (zerop end2))
384
always (and (= end1 end2)
385
(octet-vector= buffer1 buffer2
389
(defun file-size (file &key (element-type '(unsigned-byte 8)))
390
"The size of FILE, in units of ELEMENT-TYPE (defaults to bytes).
392
The size is computed by opening the file and getting the length of the
395
If all you want is to read the file's size in octets from its metadata,
396
consider FILE-SIZE-IN-OCTETS instead."
397
(check-type file (or string pathname))
398
(with-input-from-file (in file :element-type element-type)
401
(defun file-timestamp ()
402
"Returns current timestamp as a string suitable as the name of a timestamped-file."
403
(multiple-value-bind (sec min hr day mon yr)
405
(format nil "~4d~2,'0d~2,'0d_~2,'0d~2,'0d~2,'0d" yr mon day hr min sec)))
408
"Returns current date as a string suitable as the name of a timestamped-file."
409
(multiple-value-bind (sec min hr day mon yr)
411
(declare (ignore sec min hr))
412
(format nil "~4d~2,'0d~2,'0d" yr mon day)))
414
;; see https://www.n16f.net/blog/counting-lines-with-common-lisp/
416
(defvar *hidden-paths* (list ".hg" ".git"))
418
(defun hidden-path-p (path &optional strict)
419
"Return T if PATH is strictly a hidden file or directory or NIL else."
420
(declare (type pathname path))
421
(let ((name (if (directory-path-p path)
422
(car (last (pathname-directory path)))
423
(file-namestring path))))
424
(and (plusp (length name))
426
(eq (char name 0) #\.)
427
(member name *hidden-paths* :test 'equal)))))
429
(defun find-files (path &optional (hide *hidden-paths*))
430
"Return a list of all files contained in the directory at PATH or any of its
432
(declare (type (or pathname string) path))
433
(flet ((list-directory (path)
435
(make-pathname :defaults (directory-path path)
436
:type :wild :name :wild))))
438
(children (list-directory (directory-path path))))
439
(dolist (child children paths)
440
(unless (and hide (hidden-path-p child (eq t hide)))
441
(if (directory-path-p child)
442
(setf paths (append paths (find-files child)))
443
(push child paths)))))))
445
(defun count-file-lines (path)
446
"Count the number of non-empty lines in the file at PATH. A line is empty if
447
it only contains spaces or tab characters."
448
(declare (type pathname path))
449
(with-open-file (stream path :element-type '(unsigned-byte 8))
453
(let ((octet (read-byte stream nil)))
455
((or (null octet) (eq octet #.(char-code #\Newline)))
459
(return-from count-file-lines nb-lines))
461
((and (/= octet #.(char-code #\Space))
462
(/= octet #.(char-code #\Tab)))
463
(setf blank-line nil)))))))
465
(defun probe-merge-file (name path)
466
"Merge paths NAME on PATH and call PROBE-FILE on the result."
467
(probe-file (merge-pathnames name path)))
469
(defun probe-delete-file (file)
470
"Delete FILE if it exists, else return NIL."
471
(when (probe-file file)
474
(defun probe-directory (directory)
475
"Probe DIRECTORY, ensuring it is a valid directory name and exists."
476
(when-let ((dir (probe-file directory)))
477
(when (directory-path-p dir)
480
(defun probe-delete-directory (directory &key (recursive t))
481
"Delete DIRECTORY if it exists, when RECURSIVE is non-nil, delete recursively."
482
(when (probe-file directory)
483
(delete-directory directory :recursive recursive)))
485
(defun delete-directories (dirs &key (recursive t) (probe t))
486
"Delete directories in list DIRS."
489
(probe-delete-directory d :recursive recursive)
490
(sb-ext:delete-directory d :recursive recursive))))
492
(defun move-file (input output)
493
"Move file INPUT to OUTPUT."
494
(progn (uiop:copy-file input output)
495
(delete-file input)))