上一节我们了解了文件读写的一些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-functionswrite-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 .....我们现在提供一个函数按日志查看某天的工作日志。
(defunmy/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"")(foundnil)(collectingnil))(with-temp-buffer(when(file-exists-pmy/work-log-path)(insert-file-contentsmy/work-log-path)(let((lines(split-string(buffer-string)"\n"t)))(whilelines(let((line(carlines)))(cond;; 如果是日期标题行((string-prefix-p"** <"line)(ifcollecting;; 如果正在收集,说明遇到了下一个日期,停止收集并退出循环(setqlinesnil);; 否则检查是否匹配目标日期(when(string-match(concat"\\*\\* <"date)line)(setqfoundt)(setqcollectingt)(setqcontent(concatline"\n")))));; 如果是非标题行(t(whencollecting(setqcontent(concatcontent line"\n")))))(setqlines(cdrlines)))))))(if(notfound)(message"未找到 %s 的日志记录"date)(let((buffer(generate-new-buffer"WorkLog")))(switch-to-bufferbuffer)(erase-buffer)(insertcontent)))))上述代码,我们通过 with-temp-buffer 创建一个临时的buffer,用来读取数据。处理时将所有数据按行划分,找到以**开头的日期行,如果当前日期匹配上目标日期,则将found设置为真,后面连着的几行是我们实际得到的当前的日志内容,如果后续又找到了**开头的内容表示到下一个日期了应该退出。
小结
本篇我们理解了find-file-noselect在临时读取数据的弊端,并且介绍了在这种情况下的一半处理方式。本篇的api函数在上一篇已经列举过了,这次就不在说明了。