《elisp 每日一练》—— 低成本的文件读写

《elisp 每日一练》—— 低成本的文件读写

Masimaro
2026-09-04 / 0 评论 / 0 阅读 / 正在检测是否收录...

上一节我们了解了文件读写的一些api,并且通过一个保存灵感的小例子了解了Emacs中文本的操作。实际在Emacs中读写文本无外乎两种目的,第一种是用户需要编辑文本并最终保存,第二种是要读取文件内容作为参数或者内容完成后续的工作。例如利用配置文件控制Emacs的行为或者插件的行为。

对于第一种我们可以使用正规的 find-filefind-file-noselect 将文件加载到一个buffer中,并且保存的时候使用 save-buffer 进行保存。但是这种方式太重了。Emacs在正式读写文件的时候有一些附加动作,如果我们只是简单的要里面的文本,这种开销有点太大了。今天我们主要介绍使用 with-temp-fileinsert-file-contentswrite-region 进行低成本的文件读写

背景知识

在上一节我们提到 visit 参数,但是我们没有详细展开,现在我们详细展开看看 find-file-noselect 在基础的读写需求上的额外操作。

首先它会通过 visit 参数构建一个访问关系,它会创建一个新的缓冲区,或者复用一个已存在的缓冲区。最关键的是,它会将缓冲区与文件“关联”起来。这意味着它会设置 buffer-file-name 并记录文件的修改时间,后续Emacs就将这个buffer和对应的文件关联起来了。

在读取文件的时候它还会额外进行编码转化,例如将文件由其他编码转化为utf-8,或者其他规定的编码。另外它也会进行文件格式的转化,也就是将DOS 格式的 \r\n 转化为 \n

在打开文件之后,它会根据文件的类型设置文件的Major Mode。例如python文件会设置 python-mode 在该mode下可能会进行格式化、语法高亮等等操作。

在文件加载完之后,可能会调用 find-file-hook 让用户代码有机会执行额外的动作。

save-buffer 也有一些额外的动作。

首先它会检查是否真的需要保存。它会根据我们前面介绍过的 buffer 的 modified 状态来判断当前是否需要保存。

接着它会进行额外的备份,例如将文件保存为 foo~.txt 这类与源文件类似的文件。

在真正执行写入操作之前,它会运行 write-contents-functions write-file-functions 这类hook函数,以便用户代码在写入前有机会介入,控制写入行为。

写入完成之后,它会更新buffer的状态,将它更新为未修改状态,并更新文件的上次修改时间。

从上面过程看,这两个函数纯粹就是为了让用户编辑文件所设计的,它纯粹是编辑器层面的函数。如果我们只是临时的读一些像保存上次退出保存的一些状态文件,下一次启动时用于恢复,对于这类临时文件,用户甚至都不知道该文件的存在。我们如果使用 find-file-noselect 这类的函数,就有点浪费了。

这种类型它们合适的方式是读取时使用 with-temp-buffer + insert-file-contents 来进行轻量级读取。如果要写入则采用 write-region。如果你需要简单的操作buffer之后将buffer内容写入,可以使用 with-temp-buffer 。你可以将 with-temp-file 看成 with-temp-buffer + insert-file-content + write-region 的组合技。

练习

假设我们记录了多天的工作日志。每天的日志格式为:

** <2026-09-04 Fri>
content
content
content
.....

我们现在提供一个函数按日志查看某天的工作日志。

(defun my/check-daily-work (date)
  "按输入日期 DATE (格式 YYYY-MM-DD) 查看工作日志。"
  (interactive (list (read-string "Input date (YYYY-MM-DD): "
                                  (format-time-string "%Y-%m-%d"))))
  (let ((content "")
        (found nil)
        (collecting nil))
    (with-temp-buffer
      (when (file-exists-p my/work-log-path)
        (insert-file-contents my/work-log-path)
        (let ((lines (split-string (buffer-string) "\n" t)))
          (while lines
            (let ((line (car lines)))
              (cond
               ;; 如果是日期标题行
               ((string-prefix-p "** <" line)
                (if collecting
                    ;; 如果正在收集,说明遇到了下一个日期,停止收集并退出循环
                    (setq lines nil)
                  ;; 否则检查是否匹配目标日期
                  (when (string-match (concat "\\*\\* <" date) line)
                    (setq found t)
                    (setq collecting t)
                    (setq content (concat line "\n")))))
               ;; 如果是非标题行
               (t
                (when collecting
                  (setq content (concat content line "\n")))))
              (setq lines (cdr lines)))))))
    (if (not found)
        (message "未找到 %s 的日志记录" date)
      (let ((buffer (generate-new-buffer "WorkLog")))
        (switch-to-buffer buffer)
        (erase-buffer)
        (insert content)))))

上述代码,我们通过 with-temp-buffer 创建一个临时的buffer,用来读取数据。处理时将所有数据按行划分,找到以 ** 开头的日期行,如果当前日期匹配上目标日期,则将 found 设置为真,后面连着的几行是我们实际得到的当前的日志内容,如果后续又找到了 ** 开头的内容表示到下一个日期了应该退出。

小结

本篇我们理解了find-file-noselect 在临时读取数据的弊端,并且介绍了在这种情况下的一半处理方式。本篇的api函数在上一篇已经列举过了,这次就不在说明了。

0

评论 (0)

取消