暗无天日

=============>DarkSun的个人博客

从 proced 定制中学到的 Elisp 模式

Rahul Juliato 的 Getting Emacs proced.el to Show CPU and Memory on macOS 表面上是解决 macOS 上 proced 缺少 CPU/Mem 列的问题,实际上是一份很好的 Elisp 编程教学。本文从中提取六个可复用的编程模式——它们跟 proced 和 macOS 无关,你在任何需要异步调用外部命令、缓存数据、扩展第三方包的场景都能用。

模式一:异步进程 + sentinel

Elisp 中执行外部命令有两种方式: call-process (同步,会阻塞 Emacs )和 make-process (异步,不阻塞)。凡是执行时间不确定的命令,都应该用 make-process

(make-process
 :name "my-process"
 :buffer (generate-new-buffer " *my-process-temp*")
 :command '("env" "LC_ALL=C" "ps" "-eo" "pid=,%cpu=,%mem=")
 :noquery t
 :sentinel
 (lambda (proc _event)
   (when (eq (process-status proc) 'exit)
     (with-current-buffer (process-buffer proc)
       (goto-char (point-min))
       ;; 在这里处理输出
       (buffer-string))
     (kill-buffer (process-buffer proc)))))

关键设计:

  • sentinel 是一个回调函数,在进程状态变化时触发。通常只关心 'exit 状态——此时 buffer 中已有完整输出。
  • :noquery t 防止 Emacs 退出时弹出"还有进程在运行"的确认框。
  • generate-new-buffer 创建临时 buffer 承接输出,名称以空格开头( " " )的 buffer 在 list-buffers 中默认隐藏。
  • 处理完后 kill-buffer 清理临时 buffer ,避免 buffer 堆积。

模式二:hash table 做进程级缓存

当你需要频繁按 key 查找数据时,hash table 比 alist 快得多( O(1) vs O(n) )。

;; 创建
(defvar my-cache (make-hash-table))

;; 写入(用 cons 存两个值,car 和 cdr 分别取)
(puthash 1234 (cons 2.5 1.3) my-cache)
(puthash 5678 (cons 0.1 0.5) my-cache)

;; 读取
(car (gethash 1234 my-cache))   ;; => 2.5
(cdr (gethash 1234 my-cache))   ;; => 1.3
(gethash 9999 my-cache)          ;; => nil
2.5
1.3
nil

为什么用 cons 存值?因为 carcdr 是 Elisp 中最快的取值操作之一,比 plist-getalist-get 快。当你只需要存两个值时,cons 是最佳选择。

刷新时不要原地修改 hash table ,而是创建一个新的再替换——这样即使有并发的读操作也不会读到半更新的状态:

(defun my-refresh-cache ()
  (let ((new-cache (make-hash-table)))
    ;; 填充 new-cache ...
    (setq my-cache new-cache)))  ;; 原子替换

模式三: rx 宏写可读正则

Elisp 的 rx 宏用 S-expression 写正则,比字符串正则更容易读和维护。对比一下:

;; 字符串正则:不直观,需要脑内解析
"^[[:blank:]]*\\([[:digit:]]+\\)[[:blank:]]+\\([.[:digit:]]+\\)"

;; rx 宏:自解释
(rx (* blank)
    (group (+ digit))
    (+ blank)
    (group (+ (any digit ?.))))

rx 编译出来的结果跟手写字符串正则完全一样:

(rx (* blank)
    (group (+ digit))
    (+ blank)
    (group (+ (any digit ?.)))
    (+ blank)
    (group (+ (any digit ?.))))
[[:blank:]]*\([[:digit:]]+\)[[:blank:]]+\([.[:digit:]]+\)[[:blank:]]+\([.[:digit:]]+\)

常用 rx 组合:

rx 写法 匹配内容
(+ digit) 一个或多个数字
(* blank) 零或多个空白
(any digit ?.) 数字或小数点
(group ...) 捕获组,对应 match-string

模式四:timer 生命周期管理

Emacs 的 run-with-timer 可以周期性执行任务,但如果不在合适的时机取消,timer 会一直运行下去(即使对应的 buffer 已经关了)。正确的做法是在 mode hook 里启动,在 kill-buffer-hook 里取消:

(defvar my-timer nil)

;; 在 mode hook 中启动
(add-hook 'my-mode-hook
  (lambda ()
    (setq my-timer
          (run-with-timer 0 2 #'my-refresh-function))))

;; 在 kill-buffer-hook 中清理
(add-hook 'kill-buffer-hook
  (lambda ()
    (when (and (derived-mode-p 'my-mode)
               (timerp my-timer))
      (cancel-timer my-timer)
      (setq my-timer nil))))

三个要点:

  • run-with-timer 的参数是 (延迟秒数 重复间隔 函数) ,第一个参数 0 表示立即执行第一次。
  • cancel-timer 取消后要把变量设为 nil ,避免悬空引用。
  • guard 条件 (derived-mode-p 'my-mode) 确保 kill-buffer-hook 不会在其他类型的 buffer 中误触发。 kill-buffer-hook 是全局的,每次任何 buffer 关闭都会触发,所以必须有 mode 判断。

模式五: file-remote-p 做 TRAMP 感知

当你的代码依赖本地系统状态(比如执行本地 ps ),必须考虑 buffer 可能在 TRAMP 远程主机上运行的情况。 file-remote-p 检测 default-directory 是否指向远程:

(unless (file-remote-p default-directory)
  ;; 只在本地执行
  (my-run-local-command))

为什么这很重要?如果你不加检测,可能会用本地 ps 的输出作为参数在远程主机上执行命令。轻则数据显示错误,重则本地 PID 跟远程 PID 碰撞导致误操作。

模式六:通过 custom attributes 扩展第三方包

很多 Emacs 包提供 *-custom-attributes 或类似的扩展点,让你不用修改包的源码就能添加功能。 procedproced-custom-attributes 就是一个例子:它接受一个 lambda 列表,每个 lambda 接收当前行的属性 alist ,返回 (keyword . value) 就能添加新列。

(setq proced-custom-attributes
  (list
   (lambda (attrs)
     (when-let* ((pid (cdr (assq 'pid attrs)))
                 (v (my-lookup pid)))
       (cons 'my-attribute v)))))

这个模式不限于 proced 。当你需要给某个包添加自定义字段时,先看看它有没有类似的扩展点——很多设计良好的包都会提供。

小结

模式 一句话
异步进程 + sentinel make-process 代替 call-process ,在 sentinel 中处理输出
hash table 缓存 高频查找用 hash table ,刷新时整体替换而非原地修改
rx 用 S-expression 写正则,比字符串更可读更易维护
timer 生命周期 hook 启动、hook 清理、 derived-mode-p guard 三件套
file-remote-p 任何依赖本地系统状态的代码都要加 TRAMP 检测
custom attributes 用 lambda 返回 (keyword . value) 扩展第三方包的显示
Emacs : Elisp : 编程模式