Elisp 操作对象:文件

Table of Contents

打开文件的过程

当你打开一个文件时,实际上 emacs 做了很多事情:

  • 把文件名展开成为完整的文件名
  • 判断文件是否存在
  • 判断文件是否可读或者文件大小是否太大
  • 查看文件是否已经打开,是否被锁定
  • 向缓冲区插入文件内容
  • 设置缓冲区的模式

这还只是简单的一个步骤,实际情况比这要复杂的多,许多异常需要考虑。而且 为了所有函数的可扩展性,许多变量、handler 和 hook 加入到文件操作的函数 中,使得每一个环节都可以让用户或者 elisp 开发者可以定制,甚至完全接管 所有的文件操作。

这里需要区分两个概念:文件和缓冲区。它们是两个不同的对象,文件是在计算 机上可持久保存的信息,而缓冲区是 Emacs 中包含文件内容信息的对象,在 emacs 退出后就会消失,只有当保存缓冲区之后缓冲区里的内容才写到文件中去。

文件读写

打开一个文件的命令是 find-file。这命令使一个缓冲区访问某个文件,并让这个缓冲区成为当前缓冲区。在打开文件过程中会调用 find-file-hook。find-file-noselect 是所有访问文件的核心函数。与 find-file 不同,它只返回访问文件的缓冲区。这两个函数都有一个特点,如果 emacs 里已经有一个缓冲区访问这个文件的话,emacs 不会创建另一个缓冲区来访问文件,而只是简单返 回或者转到这个缓冲区。怎样检查有没有缓冲区是否访问某个文件呢?所有和文件关联的缓冲区里都有一个 buffer-local 变量 buffer-file-name。但是不要直接设置这个变量来改变缓冲区关联的文件。而是使用 set-visited-file-name 来修改。同样不要直接从 buffer-list 里搜索buffer-file-name 来查找和某个文件关联的缓冲区。应该使用 get-file-buffer 或者 find-buffer-visiting。

(find-file "~/temp/test.txt")
(with-current-buffer
    (find-file-noselect "~/temp/test.txt")
  buffer-file-name)                     ; => "/home/ywb/temp/test.txt"
(find-buffer-visiting "~/temp/test.txt") ; => #<buffer test.txt>
(get-file-buffer "~/temp/test.txt")      ; => #<buffer test.txt>

保存一个文件的过程相对简单一些。首先创建备份文件,处理文件的位模式,将 缓冲区写入文件。保存文件的命令是 save-buffer。相当于其它编辑器里另存为 的命令是 write-file。在这个过程中会调用一些函数或者 hook。 write-file-functions 和 write-contents-functions 几乎功能完全相同。它们 都是在写入文件之前运行的函数,如果这些函数中有一个返回了 non-nil 的值, 则会认为文件已经写入了,后面的函数都不会运行,而且也不会使用再调用其它 写入文件的函数。这两个变量有一个重要的区别是write-contents-functions 在 改变主模式之后会被修改,因为它没有permanent-local 属性,而 write-file-functions 则会仍然保留。before-save-hook 和 write-file-functions 功能也比较类似,但是这个变量里的函数会逐个执行,不 论返回什么值也不会影响后面文件的写入。after-save-hook 是在文件已经写入 之后才调用的 hook,它是 save-buffer 最后一个动作。

但是实际上在 elisp 编程过程中经常遇到的一个问题是读取一个文件中的内容, 读取完之后并不希望这个缓冲区还留下来,如果直接用 kill-buffer 可能会把 用户打开的文件关闭。而且 find-file-noselect 做的事情实在超出我们的需要 的。这时你可能需要的是更底层的文件读写函数,它们是 insert-file-contents 和 write-region,调用形式分别是

(insert-file-contents filename &optional visit beg end replace)
(write-region start end filename &optional append visit lockname mustbenew)

insert-file-contents 可以插入文件中指定部分到当前缓冲区中。如果指定 visit 则会标记缓冲区的修改状态并关联缓冲区到文件,一般是不用的。replace 是指是否要删除缓冲区里其它内容,这比先删除缓冲区其它内容后插入文件内容要快一些,但是一般也用不上。insert-file-contents 会处理文件的编码,如果不需要解码文件的话,可以用 insert-file-contents-literally。

write-region 可以把缓冲区中的一部分写入到指定文件中。如果指定 append 则是添加到文件末尾。和 insert-file-contents 相似,visit 参数也会把缓冲区和文件关联,lockname 则是文件锁定的名字,mustbenew 确保文件存在时会要求用户确认操作。

文件信息

文件是否存在可以使用 file-exists-p 来判断。对于目录和一般文件都可以用这个函数进行判断,但是符号链接只有当目标文件存在时才返回 t。

如何判断文件是否可读或者可写呢?file-readable-p、file-writable-p, file-executable-p 分用来测试用户对文件的权限。文件的位模式还可以用 file-modes 函数得到。

(file-exists-p "~/temp/test.txt")              ; => t
(file-readable-p "~/temp/test.txt")            ; => t
(file-writable-p "~/temp/test.txt")            ; => t
(file-executable-p "~/temp/test.txt")          ; => nil
(format "%o" (file-modes "~/temp/test.txt"))   ; => "644"

文件类型判断可以使用 file-regular-p、file-directory-p、file-symlink-p, 分别判断一个文件名是否是一个普通文件(不是目录,命名管道、终端或者其它 IO 设备)、文件名是否一个存在的目录、文件名是否是一个符号链接。其中 file-symlink-p 当文件名是一个符号链接时会返回目标文件名。文件的真实名字也就是除去相对链接和符号链接后得到的文件名可以用 file-truename 得到。 事实上每个和文件关联的 buffer 里也有一个缓冲区局部变量 buffer-file-truename 来记录这个文件名。

$ ls -l t.txt
lrwxrwxrwx 1 ywb ywb 8 2007-07-15 15:51 t.txt -> test.txt
(file-regular-p "~/temp/t.txt")         ; => t
(file-directory-p "~/temp/t.txt")       ; => nil
(file-symlink-p "~/temp/t.txt")         ; => "test.txt"
(file-truename "~/temp/t.txt")          ; => "/home/ywb/temp/test.txt"

文件更详细的信息可以用 file-attributes 函数得到。这个函数类似系统的 stat 命令,返回文件几乎所有的信息,包括文件类型,用户和组用户,访问日 期、修改日期、status change 日期、文件大小、文件位模式、inode number、 system number。这是我写的方便使用的帮助函数:

(defun file-stat-type (file &optional id-format)
  (car (file-attributes file id-format)))
(defun file-stat-name-number (file &optional id-format)
  (cadr (file-attributes file id-format)))
(defun file-stat-uid (file &optional id-format)
  (nth 2 (file-attributes file id-format)))
(defun file-stat-gid (file &optional id-format)
  (nth 3 (file-attributes file id-format)))
(defun file-stat-atime (file &optional id-format)
  (nth 4 (file-attributes file id-format)))
(defun file-stat-mtime (file &optional id-format)
  (nth 5 (file-attributes file id-format)))
(defun file-stat-ctime (file &optional id-format)
  (nth 6 (file-attributes file id-format)))
(defun file-stat-size (file &optional id-format)
  (nth 7 (file-attributes file id-format)))
(defun file-stat-modes (file &optional id-format)
  (nth 8 (file-attributes file id-format)))
(defun file-stat-guid-changep (file &optional id-format)
  (nth 9 (file-attributes file id-format)))
(defun file-stat-inode-number (file &optional id-format)
  (nth 10 (file-attributes file id-format)))
(defun file-stat-system-number (file &optional id-format)
  (nth 11 (file-attributes file id-format)))
(defun file-attr-type (attr)
  (car attr))
(defun file-attr-name-number (attr)
  (cadr attr))
(defun file-attr-uid (attr)
  (nth 2 attr))
(defun file-attr-gid (attr)
  (nth 3 attr))
(defun file-attr-atime (attr)
  (nth 4 attr))
(defun file-attr-mtime (attr)
  (nth 5 attr))
(defun file-attr-ctime (attr)
  (nth 6 attr))
(defun file-attr-size (attr)
  (nth 7 attr))
(defun file-attr-modes (attr)
  (nth 8 attr))
(defun file-attr-guid-changep (attr)
  (nth 9 attr))
(defun file-attr-inode-number (attr)
  (nth 10 attr))
(defun file-attr-system-number (attr)
  (nth 11 attr))

前一组函数是直接由文件名访问文件信息,而后一组函数是由 file-attributes 的返回值来得到文件信息。

修改文件信息

重命名和复制文件可以用 rename-file 和 copy-file。删除文件使用 delete-file。创建目录使用 make-directory 函数。不能用 delete-file 删除 目录,只能用 delete-directory 删除目录。当目录不为空时会产生一个错误。

设置文件修改时间使用 set-file-times。设置文件位模式可以用 set-file-modes 函数。set-file-modes 函数的参数必须是一个整数。你可以用位 函数 logand、logior 和 logxor 函数来进行位操作。

文件名操作

虽然 MSWin 的文件名使用的路径分隔符不同,但是这里介绍的函数都能用于 MSWin 形式的文件名,只是返回的文件名都是 Unix 形式了:

(file-name-directory "~/temp/test.txt")      ; => "~/temp/"
(file-name-nondirectory "~/temp/test.txt")   ; => "test.txt"
(file-name-sans-extension "~/temp/test.txt") ; => "~/temp/test"
(file-name-extension "~/temp/test.txt")      ; => "txt"
(file-name-sans-versions "~/temp/test.txt~") ; => "~/temp/test.txt"
(file-name-sans-versions "~/temp/test.txt.~1~") ; => "~/temp/test.txt"
(file-name-sans-extension (file-name-nondirectory "~/temp/test.txt")) ; => "test"

路径如果是从根目录开始的称为是绝对路径。测试一个路径是否是绝对路径使用 file-name-absolute-p。如果在 Unix 或 GNU/Linux 系统,以 ~ 开头的路径也是绝对路径。

如果不是绝对路径,可以使用 expand-file-name 来得到绝对路径。把一个绝对路径转换成相对某个路径的相 对路径的可以用 file-relative-name 函数。

(file-name-absolute-p "~/.emacs.d")              ; => t
(file-name-absolute-p "/Users/haoran/.emacs.d")  ; => t
(expand-file-name "~/.emacs.d")                  ; => "/Users/haoran/.emacs.d"
(expand-file-name "foo" "/usr/spool/")           ; => "/usr/spool/foo"
(file-relative-name "/foo/bar" "/foo/") ; => "bar"
(file-relative-name "/foo/bar" "/hack/") ; => "../foo/bar"

对于目录, 如果要将其作为目录,也就是确保它是以路径分隔符结束 ,可以用 file-name-as-directory 。不要用 (concat dir “/”) 来转换,这会有移植问题。 和它相对应的函数是 directory-file-name

(file-name-as-directory "~rms/lewis")   ; => "~rms/lewis/"
(directory-file-name "~lewis/")         ; => "~lewis"

emacs 中与默认目录相关的函数:

(locate-user-emacs-file "lisp") ;;=> "~/.emacs.d/lisp"

如果要得到所在系统使用的文件名,可以用 convert-standard-filename。比如 在 MSWin 系统上,可以用这个函数返回用 “\” 分隔的文件名

(convert-standard-filename "c:/windows")  ;=> "c:\\windows"

临时文件

如果需要产生一个临时文件,可以使用 make-temp-file。这个函数按给定前缀产 生一个不和现有文件冲突的文件,并返回它的文件名。如果给定的名字是一个相 对文件名,则产生的文件名会用 temporary-file-directory 进行扩展。也可以 用这个函数产生一个临时文件夹。如果只想产生一个不存在的文件名,可以用 make-temp-name 函数

(make-temp-file "foo")                  ; => "/tmp/foo5611dxf"
(make-temp-name "foo")                  ; => "foo5611q7l"

读取目录内容

可以用 directory-files 来得到某个目录中的全部或者符合某个正则表达式的 文件名。

(directory-files "~/temp/dir/")
;; =>
;; ("#foo.el#" "." ".#foo.el" ".." "foo.el" "t.pl" "t2.pl")
(directory-files "~/temp/dir/" t)
;; =>
;; ("/home/ywb/temp/dir/#foo.el#"
;;  "/home/ywb/temp/dir/."
;;  "/home/ywb/temp/dir/.#foo.el"
;;  "/home/ywb/temp/dir/.."
;;  "/home/ywb/temp/dir/foo.el"
;;  "/home/ywb/temp/dir/t.pl"
;;  "/home/ywb/temp/dir/t2.pl")
(directory-files "~/temp/dir/" nil "\\.pl$") ; => ("t.pl" "t2.pl")

directory-files-and-attributes 和 directory-files 相似,但是返回的列表 中包含了 file-attributes 得到的信息。file-name-all-versions 用于得到某 个文件在目录中的所有版本,file-expand-wildcards 可以用通配符来得到目录 中的文件列表。