14.9.2 创建文件列表

函数 recursive-lengths-list-many-files 需要一个文件列表作为其参数。在我们的测试示例中,我们手工构建了这样一个列表;但是 Emacs Lisp 源代码目录太大,我们无法手动创建。相反,我们将编写一个函数来为我们完成这个任务。在这个函数中,我们将同时使用 while 循环和递归调用。

在旧版本的 GNU Emacs 中,我们无需编写这样的函数,因为它们将所有的 ‘.el’ 文件放在一个目录中。相反,我们可以使用 directory-files 函数,该函数列出了匹配指定模式的文件的名称,该模式在单个目录中。

然而,最近的 Emacs 版本将 Emacs Lisp 文件放在顶层 lisp 目录的子目录中。这种重新排列方便了导航。例如,所有与邮件相关的文件都在名为 maillisp 子目录中。但与此同时,这种排列方式迫使我们创建一个能够进入子目录的文件列表函数。

我们可以使用熟悉的函数,如 carnthcdrsubstring,以及一个名为 directory-files-and-attributes 的现有函数来创建这个函数。这个后者不仅列出目录中所有文件的名称,包括子目录的名称,还列出它们的属性。

重新表述我们的目标:创建一个函数,使我们能够将文件名作为一个列表传递给 recursive-lengths-list-many-files,该列表看起来像这样(但元素更多):

("./lisp/macros.el"
 "./lisp/mail/rmail.el"
 "./lisp/hex-util.el")

directory-files-and-attributes 函数返回一个列表的列表。主列表中的每个列表都包含13个元素。列表的第一个元素是包含文件的名称的字符串,在 GNU/Linux 中,它可能是一个 目录文件,也就是说,具有目录的特殊属性的文件。列表的第二个元素是目录的 t,符号链接的字符串(字符串是链接到的名称),或 nil

例如,lisp/ 目录中的第一个 ‘.el’ 文件是 abbrev.el。它的名称是 /usr/local/share/emacs/22.1.1/lisp/abbrev.el,它既不是目录也不是符号链接。

以下是 directory-files-and-attributes 如何列出该文件及其属性的方式:

("abbrev.el"
nil
1
1000
100
(20615 27034 579989 697000)
(17905 55681 0 0)
(20615 26327 734791 805000)(15)
13188
"-rw-r--r--"
t
2971624
773)

另一方面,mail/lisp/ 目录中的一个子目录。其列表的开头看起来像这样:

("mail"
t
…
)

(要了解不同属性,请查看 file-attributes 的文档。请注意,file-attributes 函数不会列出文件名,因此它的第一个元素是 directory-files-and-attributes 的第二个元素。)

我们希望我们的新函数 files-in-below-directory 能够列出它被告知检查的目录以及该目录下的任何子目录中的 ‘.el’ 文件。

这为我们提供了构建 files-in-below-directory 的线索:在目录内,该函数应该将 ‘.el’ 文件名添加到列表中;而如果在目录内,该函数遇到子目录,它应该进入该子目录并重复其操作。

然而,我们应该注意到每个目录都包含一个指向自身的名称,称为 .(“点”),以及一个指向其父目录的名称,称为 ..(“点 点”)。 (在 /,即根目录中,.. 指向它自己,因为 / 没有父目录。)显然,我们不希望我们的 files-in-below-directory 函数进入这些目录,因为它们始终直接或间接地导致当前目录。

因此,我们的 files-in-below-directory 函数必须执行几个任务:

让我们编写一个函数定义来执行这些任务。我们将使用一个 while 循环来在目录中从一个文件名移动到另一个文件名,检查需要执行的操作;我们将使用递归调用来在每个子目录上重复这些操作。递归模式是 Accumulate(see 递归模式:累积),使用 append 作为组合器。

以下是该函数:

(defun files-in-below-directory (directory)
  "列出 DIRECTORY 及其子目录中的 .el 文件。"
  ;; 尽管该函数将被非交互地使用,
  ;; 但如果我们将其设置为交互式,测试将更容易。
  ;; 目录的名称将类似于
  ;; "/usr/local/share/emacs/22.1.1/lisp/"
  (interactive "DDirectory name: ")
  (let (el-files-list
        (current-directory-list
         (directory-files-and-attributes directory t)))
    ;; 当我们在当前目录中时
    (while current-directory-list
      (cond
       ;; 检查文件名是否以 '.el' 结尾
       ;; 如果是,则将其名称添加到列表中。
       ((equal ".el" (substring (car (car current-directory-list)) -3))
        (setq el-files-list
              (cons (car (car current-directory-list)) el-files-list)))
       ;; 检查文件名是否是目录的名称
       ((eq t (car (cdr (car current-directory-list))))
        ;; 决定是跳过还是递归
        (if
            (equal "."
                   (substring (car (car current-directory-list)) -1))
            ;; 然后什么都不做,因为文件名是
            ;;   当前目录或父目录的名称,"." 或 ".."
            ()
          ;; 否则,进入该目录并重复过程
          (setq el-files-list
                (append
                 (files-in-below-directory
                  (car (car current-directory-list)))
                 el-files-list)))))
      ;; 移动到列表中的下一个文件名;这也
      ;; 缩短列表,因此 while 循环最终会结束
      (setq current-directory-list (cdr current-directory-list)))
    ;; 返回文件名
    el-files-list))

files-in-below-directory directory-files 函数接受一个参数,即目录的名称。

因此,在我的系统上,

(length
 (files-in-below-directory "/usr/local/share/emacs/22.1.1/lisp/"))

告诉我,在我的 Lisp 源代码目录及其子目录中有 1031 个 ‘.el’ 文件。

files-in-below-directory 以字母顺序的逆序返回一个列表。将列表按字母顺序排序的表达式如下:

(sort
 (files-in-below-directory "/usr/local/share/emacs/22.1.1/lisp/")
 'string-lessp)

Footnotes

(15)

如果 current-time-listnil,则三个时间戳分别为 (1351051674579989697 . 1000000000)(1173477761000000000 . 1000000000)(1351050967734791805 . 1000000000)