本章将使用到的库有:

当然,Common Lisp 自身也有相应的操作:

检查文件是否存在

probe-file

  1. $ ln -s /etc/passwd foo
  2. * (probe-file "/etc/passwd")
  3. #p"/etc/passwd"
  4. * (probe-file "foo")
  5. #p"/etc/passwd"
  6. * (probe-file "bar")
  7. NIL

创建目录

[ensure-directories-exist](http://www.lispworks.com/documentation/HyperSpec/Body/f_ensu_1.htm)

  1. (ensure-directories-exist "foo/bar/baz/")

该代码将会创建 foobarbaz,注意最后需要有个斜杆(/)。

打开文件

[open](http://www.lispworks.com/documentation/HyperSpec/Body/f_open.htm)[close](http://www.lispworks.com/documentation/HyperSpec/Body/f_close.htm),通常更推荐使用 [with-open-file](http://www.lispworks.com/documentation/HyperSpec/Body/m_w_open.htm)。其中 open 需要手动将文件关闭,而with-open-file 会在执行代码完后自动将文件关闭。

  1. (with-open-file (str <_file-spec_>
  2. :direction <_direction_>
  3. :if-exists <_if_exists_>
  4. :if-does-not-exist <_if-does-not-exist_>)
  5. <_your code here_>)
  • str:绑定打开的文件的变量
  • <_file-spec_>:文件名或路径
  • <_direction_>:input 表示读取文件,:output 表示写入文件,:io表示同时读写,默认的话为::input
  • <_if_exists_>:当文件存在时的一些操作。如果只是对文件进行读取的话,可以忽略。:error 表示错误信号,为默认。:supersede 表示替换原有的文件, :append 在文件后追加,:rename 重命名文件,nil 将流文件绑定为 nil
  • <_if-does-not-exist_>:当文件不存在时,:error 错误信息,:create 创建一个空文件,nil 将流文件绑定为 nil

with-open-file 还有很选项,详情参见 the CLHS entry for [open](http://www.lispworks.com/documentation/HyperSpec/Body/f_open.htm)

读取文件

将文件内容读入字符串或是列表

  1. (uiop:read-file-string "file.txt")

  1. (uiop:read-file-lines "file.txt")

这个可以使用 read-lineread-char 来实现,但不是个很好的方案,可以使用如下代码:

  1. (with-output-to-string (out)
  2. (with-open-file (in "/path/to/big/file")
  3. (loop with buffer = (make-array 8192 :element-type 'character)
  4. for n-characters = (read-sequence buffer in)
  5. while (< 0 n-characters)
  6. do (write-sequence buffer out :start 0 :end n-characters))))

以 utf-8 编码格式读取

为了避免 ASCII stream decoding error 错误,可以指定读取文件时的编码格式:

  1. (with-open-file (in "/path/to/big/file"
  2. :external-format :utf-8)
  3. ...

设置 SBCL 默认编码为 utf-8

~/.sbclrc 中加入下面这句:

  1. (setf sb-impl::*default-external-format* :utf-8)

如果需要的话,也可以加上

  1. (setf sb-alien::*default-c-string-external-format* :utf-8)

逐行读取

  1. (with-open-file (stream "/etc/passwd")
  2. (do ((line (read-line stream nil)
  3. (read-line stream nil)))
  4. ((null line))
  5. (print line)))
  1. (with-open-file (stream "/etc/passwd")
  2. (loop for line = (read-line stream nil 'foo)
  3. until (eq line 'foo)
  4. do (print line)))

逐字读取
  1. (with-open-file (stream "/etc/passwd")
  2. (do ((char (read-char stream nil)
  3. (read-char stream nil)))
  4. ((null char))
  5. (print char)))

逐次读取

peek-char

  1. CL-USER> (with-input-from-string (stream "I'm not amused")
  2. (print (read-char stream))
  3. (print (peek-char nil stream))
  4. (print (read-char stream))
  5. (values))
  6. #\I
  7. #\'
  8. #\'

peek-char 的第一个参数为 T 时,将会跳过空白符

  1. CL-USER> (with-input-from-string (stream "I'm not amused")
  2. (print (read-char stream))
  3. (print (read-char stream))
  4. (print (read-char stream))
  5. (print (peek-char t stream))
  6. (print (read-char stream))
  7. (print (read-char stream))
  8. (values))
  9. #\I
  10. #\'
  11. #\m
  12. #\n
  13. #\n
  14. #\o

peek-char 的第一个参数为字符时,会自动去匹配

  1. CL-USER> (with-input-from-string (stream "I'm not amused")
  2. (print (read-char stream))
  3. (print (peek-char #\a stream))
  4. (print (read-char stream))
  5. (print (read-char stream))
  6. (values))
  7. #\I
  8. #\a
  9. #\a
  10. #\m
  1. CL-USER> (with-input-from-string (stream "I'm not amused")
  2. (print (read-char stream))
  3. (print (peek-char #\d stream))
  4. (print (read-char stream))
  5. (print (peek-char nil stream nil 'the-end))
  6. (values))
  7. #\I
  8. #\d
  9. #\d
  10. THE-END
  11. CL-USER> (with-input-from-string (stream "I'm not amused")
  12. (let ((c (read-char stream)))
  13. (print c)
  14. (unread-char c stream)
  15. (print (read-char stream))
  16. (values)))
  17. #\I
  18. #\I

随机访问文件

file-position

  1. CL-USER> (with-input-from-string (stream "I'm not amused")
  2. (print (file-position stream))
  3. (print (read-char stream))
  4. (print (file-position stream))
  5. (file-position stream 4)
  6. (print (file-position stream))
  7. (print (read-char stream))
  8. (print (file-position stream))
  9. (values))
  10. 0
  11. #\I
  12. 1
  13. 4
  14. #\n
  15. 5

写入文件

  1. (with-open-file (f <pathname> :direction :output
  2. :if-exists :supersede
  3. :if-does-not-exist :create)
  4. (write-sequence s f)))

str 库中有个简易的函数

  1. (str:to-file "file.txt" content) ;; with optional options

获取文件后缀名

  1. (pathname-type "~/foo.org") ;; => "org"

获取文件属性

  1. (ql:quickload :osicat)
  2. (use-package :osicat)
  3. (let ((stat (osicat-posix:stat #P"./files.md")))
  4. (osicat-posix:stat-size stat)) ;; => 10629

还有其他的一些方法:

  1. osicat-posix:stat-dev
  2. osicat-posix:stat-gid
  3. osicat-posix:stat-ino
  4. osicat-posix:stat-uid
  5. osicat-posix:stat-mode
  6. osicat-posix:stat-rdev
  7. osicat-posix:stat-size
  8. osicat-posix:stat-atime
  9. osicat-posix:stat-ctime
  10. osicat-posix:stat-mtime
  11. osicat-posix:stat-nlink
  12. osicat-posix:stat-blocks
  13. osicat-posix:stat-blksize

列出文件和目录

  1. (namestring #p"/foo/bar/baz.txt") ==> "/foo/bar/baz.txt"
  2. (directory-namestring #p"/foo/bar/baz.txt") ==> "/foo/bar/"
  3. (file-namestring #p"/foo/bar/baz.txt") ==> "baz.txt"

列出目录中的文件

  1. (uiop:directory-files "./")

列出子目录

  1. (uiop:subdirectories "./")

遍历目录

  1. (defparameter *dirs* nil "All recursive directories.")
  2. (uiop:collect-sub*directories "~/cl-cookbook"
  3. (constantly t)
  4. (constantly t)
  5. (lambda (it) (push it *dirs*)))
  1. (cl-fad:walk-directory "./"
  2. (lambda (name)
  3. (format t "~A~%" name)))

匹配文件

  1. (remove-if-not (lambda (it)
  2. (search "App" (namestring it)))
  3. (uiop:directory-files "./"))
  1. (directory #P"*.jpg")
  2. (directory #P"**/*.png")

修改默认路径

  1. (let ((*default-pathname-defaults* (pathname "/bin/")))
  2. (directory "*sh"))
  3. (#P"/bin/zsh" #P"/bin/tcsh" #P"/bin/sh" #P"/bin/ksh" #P"/bin/csh" #P"/bin/bash")