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

《elisp 每日一练》—— 低成本的文件读写 上一节我们了解了文件读写的一些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-bufferinsert-file-contents来进行轻量级读取。如果要写入则采用write-region。如果你需要简单的操作buffer之后将buffer内容写入可以使用with-temp-buffer。你可以将with-temp-file看成with-temp-bufferinsert-file-contentwrite-region的组合技。练习假设我们记录了多天的工作日志。每天的日志格式为:** 2026-09-04 Fri content content content .....我们现在提供一个函数按日志查看某天的工作日志。(defunmy/check-daily-work(date)按输入日期DATE(格式 YYYY-MM-DD) 查看工作日志。(interactive(list(read-stringInput 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)\nt)))(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-bufferWorkLog)))(switch-to-bufferbuffer)(erase-buffer)(insertcontent)))))上述代码我们通过 with-temp-buffer 创建一个临时的buffer用来读取数据。处理时将所有数据按行划分找到以**开头的日期行如果当前日期匹配上目标日期则将found设置为真后面连着的几行是我们实际得到的当前的日志内容如果后续又找到了**开头的内容表示到下一个日期了应该退出。小结本篇我们理解了find-file-noselect在临时读取数据的弊端并且介绍了在这种情况下的一半处理方式。本篇的api函数在上一篇已经列举过了这次就不在说明了。