在之前一篇文章中,我们了解了通过Emacs异步调用进程,防止耗时进程卡住主界面。但是细心的读者会发现之前介绍的方法有一个很大的问题:我们的输出都是从buffer中获取的,创建buffer会额外消耗资源,另外如果要利用外部进程输出的内容做一些事情,我们需要固定的从buffer中读取。但是我们怎么知道buffer中有内容或者和上次相比有新内容出现,这就需要我们自己主动的不断去访问buffer。或者等待进程结束之后一次性读取所有内容。这样似乎又变成了需要等待的同步事件。对于这种场景,最理想的方式就是注册一个异步处理方式,只有在有新内容的时候触发。要实现这个需求,Emacs 为我们提供了 Process Filter 机制
背景知识
所谓的 Process Filter 是 Emacs 异步进程架构中的信息拦截器。它的工作原理是,Emacs调用进程时,进程的标准输出会被操作系统管道推送到Emacs端。Emacs 捕获到这些增量数据后,会立刻中断并调用你注册的 Filter 函数,将刚吐出的文本片段(String 增量)传递给你。如果不指定,那么会调用默认的 Filter 函数,该函数只是机械的将新内容写回到指定的buffer中。
一个标准的 Filter 回调函数的签名通常如下:
(defun my-custom-filter (proc output-string)
)其中 proc 是当前的进程对象。而output-string 则是当前增量更新的程序输出。
这里请注意, output-string 并不保证数据是完整一行或者是完整的结构,它只是机械的将本次输出的内容传进来。有时候数据会出现残缺,所以我们在处理的时候需要关注数据的完整性,有时候需要自己处理恢复完整数据,这算是另一种形式的“粘包”
filter 函数的注册可以利用我们在前面介绍 make-process 的时候提到的 :filter 关键字
练习
我们对昨天的程序做一个简单的变更。cmake 编译会输出大量信息,一般来讲我们只关心当前编译的进度以及错误出现时的报错信息。如果cmake 大量输出,Emacs需要频繁往buffer中写入新内容并刷新显示可能会造成卡顿。因此我们利用filter实现按需分批次写入缓冲区的功能。
(defvar my-build-progress nil
"当前构建进度百分比,nil 表示未在构建。")
(defun my-build-progress-filter (proc output)
"节流并解析 OUTPUT 中的进度,同时把完整输出写入进程缓冲区。"
(let ((buf (process-buffer proc)))
(when (buffer-live-p buf)
(with-current-buffer buf
(goto-char (point-max))
(insert output))))
(let ((start 0)
(last-pct nil))
(while (string-match "\\([0-9]+\\)%" output start)
(setq last-pct (string-to-number (match-string 1 output)))
(setq start (match-end 0)))
(when (and last-pct (not (equal last-pct my-build-progress))) ;; 只有进度更新时才打印信息
(setq my-build-progress last-pct)
(message "构建进度: %d%%" last-pct))))
(defun my-cmake-msvc-build (source-dir)
"在 SOURCE-DIR 下执行 CMake 配置与构建。"
(interactive "D选择项目根目录:")
(let* ((default-directory (file-name-as-directory source-dir))
(build-dir "build")
(out-buf (get-buffer-create "*compile-build*"))
(err-buf (get-buffer-create "*compile-error*"))
(config-proc nil)
(build-proc nil))
;; 错误缓冲区启用 compilation-minor-mode,支持跳转
(with-current-buffer err-buf
(erase-buffer)
(compilation-minor-mode t))
;; 构建输出缓冲区也启用编译模式,兼容走 stdout 的链接错误
(with-current-buffer out-buf
(erase-buffer)
(compilation-minor-mode t))
(setq my-build-progress nil)
(force-mode-line-update)
(message "开始 CMake 配置...")
(setq config-proc
(make-process
:name "cmake-config"
:command (list "cmake" "." "-B" build-dir
"-G" "Visual Studio 18 2026"
"-T" "v143"
"-A" "x64"
"-DCMAKE_BUILD_TYPE=Debug"
"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL")
:buffer out-buf
:stderr err-buf
:noquery t
:sentinel
(lambda (p event)
(when (memq (process-status p) '(exit signal))
(if (zerop (process-exit-status p))
(progn
(message "配置成功,开始编译...")
(setq build-proc
(make-process
:name "cmake-build"
:command (list "cmake" "--build" build-dir)
:buffer out-buf
:stderr err-buf
:filter #'my-build-progress-filter
:noquery t
:sentinel
(lambda (p1 event1)
(when (memq (process-status p1) '(exit signal))
(setq my-build-progress nil)
(if (zerop (process-exit-status p1))
(message "构建成功")
(message "构建失败,查看 *compile-error*")
(display-buffer
(if (> (with-current-buffer err-buf (buffer-size)) 0)
err-buf
out-buf))))))))
(message "CMake 配置失败,查看 *compile-error*")
(display-buffer err-buf)))))
config-proc))本次我们添加了一个名为 my-build-progress-filter 的过滤函数,它主要的作用是将新增的内容插入buffer、并且提取里面进度信息。虽然这里频繁的插入信息到buffer中,但是如果当前Emacs不显示buffer的话,它并不会立即重绘并刷新buffer,只有在调用 message 或者 force-mode-line-update 这种显式刷新的函数才会刷新当前buffer显示。这里只有在进度变化时才刷新,算是降低了刷新频率。
后面编译进程相关的代码就与前面的类似。只是通过 :filter 指定了函数
总结
本篇我们讨论了 make-process 中的 :filter 用法,绑定一个函数作为回调,每当进程有新的输出时被调用,该函数可以进行数据的解读或者控制数据插入buffer中的行为。函数需要接受两个参数:proc 是对应的进程对象、output 是本次调用新增的字符串
评论 (0)