上一节我们了解了文件读写的一些api,并且通过一个保存灵感的小例子了解了Emacs中文本的操作。实际在Emacs中读写文本无外乎两种目的,第一种是用户需要编辑文本并最终保存,第二种是要读取文件内容作为参数或者内容完成后续的工作。例如利用配置文件控制Emacs的行为或者插件的行为。
对于第一种我们可以使用正规的 find-file 和 find-file-noselect 将文件加载到一个buffer中,并且保存的时候使用 save-buffer 进行保存。但是这种方式太重了。Emacs在正式读写文件的时候有一些附加动作,如果我们只是简单的要里面的文本,这种开销有点太大了。今天我们主要介绍使用 with-temp-file 、insert-file-contents 、write-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)