惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
aimingoo的专栏
aimingoo的专栏
Martin Fowler
Martin Fowler
C
Check Point Blog
G
Google Developers Blog
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
MongoDB | Blog
MongoDB | Blog
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
D
Docker
Hugging Face - Blog
Hugging Face - Blog
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
Recent Announcements
Recent Announcements
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
Stack Overflow Blog
Stack Overflow Blog
Vercel News
Vercel News

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日
从 proced 定制中学到的 Elisp 模式
2026-04-25 · via 暗无天日

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))

(puthash 1234 (cons 2.5 1.3) my-cache)
(puthash 5678 (cons 0.1 0.5) my-cache)

(car (gethash 1234 my-cache))   (cdr (gethash 1234 my-cache))   (gethash 9999 my-cache)          
2.5
1.3
nil

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

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

(defun my-refresh-cache ()
  (let ((new-cache (make-hash-table)))
        (setq my-cache new-cache)))  

模式三: rx 宏写可读正则

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

"^[[:blank:]]*\\([[:digit:]]+\\)[[:blank:]]+\\([.[:digit:]]+\\)"

(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)

(add-hook 'my-mode-hook
  (lambda ()
    (setq my-timer
          (run-with-timer 0 2 #'my-refresh-function))))

(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) 扩展第三方包的显示