Emacs 缓存文件大整理,用一个 alist 管住所有散落状态
目录
打开你的 ~/.emacs.d ,看看里面有什么。
bookmarks、auto-saves、recentf、savehist、transient、tramp、url、multisession、image-dired、erc、rcirc……乱七八糟一堆文件。这些文件散落各处,想清不敢清,想管没头绪。
Rahul M. Juliato 在 Taming Emacs Cache and Temporary Files 一文中,给出了一套不依赖任何外部包的整理方案,把所有这些缓存文件集中管起来。
设计思路,一个 alist 驱动一切
Emacs 内部有一堆变量控制各类缓存文件的存放位置。比如 bookmark-default-file 管书签存哪, recentf-save-file 管最近打开的文件列表存哪, tramp-persistency-file-name 管 TRAMP 的连接信息存哪,等等。每个变量各自指向一个路径,默认散落在不同目录中。
为了解决这个问题,我们可以:
- 定义一个 alist,把变量名映射到相对路径
- 一个辅助函数把 key 解析成绝对路径
- 另一个辅助函数在Emacs启动时自动创建所有需要的目录,相当于
mkdir -p - 用
use-package :custom把所有变量指向这些路径
这样一来, 我们可以把所有缓存文件放到一个目录中进行管理,想清空就 rm -rf ,想备份就整个目录打包。
一,先定义缓存文件存放的根目录
这里用 defcustom ,不是 defvar 来定义根目录。
defcustom 是 Emacs 中专门定义「用户可配置选项」的宏。它和 defvar 最大的区别,是 defcustom 定义的变量会出现在 M-x customize 界面里,你可以用图形化的方式进行修改,还能把值持久化到 custom.el 中。这样一来,你不需要手工修改配置文件就能切换缓存位置。
(defcustom my/cache-directory (expand-file-name "cache/" user-emacs-directory) "Base directory for Emacs cache files. All entries in `my/cache-paths' are resolved relative to this directory. Choose one of the presets or supply any custom path. Changes take effect after restarting Emacs." :type `(choice (const :tag "Inside Emacs config (cache/ in user-emacs-directory)" ,(expand-file-name "cache/" user-emacs-directory)) (const :tag "System temp (/tmp/emacs-cache/)" "/tmp/emacs-cache/") (directory :tag "Custom directory")) :group 'my)
三个预设选项:
- config 内 (默认),缓存放
~/.emacs.d/cache/,和配置放一起,持久化留存 - /tmp ,缓存放
/tmp/emacs-cache/,重启即清空,适合录屏或测试 - 自定义路径 ,随便指到哪
注意默认值用的是 user-emacs-directory 而不是硬编码 ~/.emacs.d/ 。原因是 Emacs 29 引入了 --init-directory 命令行参数,你可以 emacs --init-directory=/path/to/config 这样启动 Emacs 。用 user-emacs-directory 锚定路径,缓存会跟着当前启动的配置走,而不是死板的都往默认 ~/.emacs.d 里写。
最后还需要一个小补丁,需要在 init.el 中指明重新加载 custom-file 。
(when custom-file
(load custom-file 'noerror 'nomessage))
二,alist 作为唯一配置源
第二个组件是一个 alist,用变量名作 key,相对路径作 value。整个方案里所有「什么东西该放哪」的信息都集中在这一处。
(defvar my/cache-paths '( ;; 文件类 (bookmark-file . "bookmarks") (ielm-history-file-name . "ielm-history.eld") (project-list-file . "projects") (recentf-save-file . "recentf") (savehist-file . "history") (save-place-file . "saveplace") (transient-history-file . "transient/history.el") (transient-levels-file . "transient/levels.el") (transient-values-file . "transient/values.el") (tramp-persistency-file-name . "tramp") (nsm-settings-file . "network-security.data") ;; 目录类(注意尾部斜杠) (auto-saves . "auto-saves/") (auto-saves-sessions . "auto-saves/sessions/") (multisession-directory . "multisession/") (url-configuration-directory . "url/") (image-dired-dir . "image-dired/") (erc-log-channels-directory . "erc/logs/") (erc-image-cache-directory . "erc/images/") (rcirc-log-directory . "rcirc/logs/")) "Alist of (KEY . RELATIVE-PATH) for cache locations. RELATIVE-PATH is resolved against `my/cache-directory'. A trailing slash on RELATIVE-PATH marks the entry as a directory.")
这里遵照 函数的约定,value 以 / 结尾的是目录,否则是文件。后面的辅助函数会根据这个来区分要不要自动创建目录。
这里作者故意把 tree-sitter/ 、 eln-cache/ 、 eshell/ 排除在外。因为 tree-sitter 和 eln-cache 的内容来自耗时较长的编译过程(语法树编译、本地代码编译),重新生成成本太大。 eshell 里的是命令历史和别名,更接近 dotfile,算不上临时状态。大家的工作流可能不同,按需增减就行。
三,两个辅助函数
两个辅助函数,一个解析路径,一个创建目录。
(defun my/cache--path (key) "Return the absolute path for KEY in `my/cache-paths'." (let ((rel (cdr (assq key my/cache-paths)))) (unless rel (error "my/cache--path: Unknown key %S" key)) (expand-file-name rel my/cache-directory)))
my/cache--path 用 assq 查找 key。 assq 对 symbol key 用 eq 做比较(比 assoc 的 equal 更快)。 unless 分支在你 :custom 块里打错了变量名的情况下给你错误提示。没有这个错误检查的话,Emacs 会静默地把值设成 nil ,然后很多包在 nil 路径上写文件,会出现莫名其妙的错误。
(defun my/cache--ensure-dirs () "Create every directory referenced by `my/cache-paths'. Entries ending in `/' are created directly; other entries have their parent directory created." (dolist (entry my/cache-paths) (let* ((abs (my/cache--path (car entry))) (dir (if (directory-name-p abs) abs (file-name-directory abs)))) (make-directory dir t)))) (my/cache--ensure-dirs)
my/cache--ensure-dirs 在启动时跑一遍,确保所有需要的目录都存在。 make-directory 的第二个参数 t 相当于 mkdir -p ,所以 transient/history.el 会自动创建 <cache>/transient/ 目录,哪怕 alist 里没单独列出它。
拼接起来
把三部分内容拼接起来就行了。
大部分缓存变量可以直接在 use-package emacs 的 :custom 块里设:
(use-package emacs :ensure nil :custom (bookmark-file (my/cache--path 'bookmark-file)) (ielm-history-file-name (my/cache--path 'ielm-history-file-name)) (project-list-file (my/cache--path 'project-list-file)) (recentf-save-file (my/cache--path 'recentf-save-file)) (savehist-file (my/cache--path 'savehist-file)) (save-place-file (my/cache--path 'save-place-file)) (transient-history-file (my/cache--path 'transient-history-file)) (transient-levels-file (my/cache--path 'transient-levels-file)) (transient-values-file (my/cache--path 'transient-values-file)) (nsm-settings-file (my/cache--path 'nsm-settings-file)) (multisession-directory (my/cache--path 'multisession-directory)) (url-configuration-directory (my/cache--path 'url-configuration-directory)) )
特殊变量处理
:custom 块适合大部分变量,但有几种情况需要单独处理。
auto-save
auto-save 用的不是单值变量,而是路径变换规则:
(setq auto-save-list-file-prefix (my/cache--path 'auto-saves-sessions) auto-save-file-name-transforms `((".*" ,(my/cache--path 'auto-saves) t)))
auto-save-list-file-prefix 控制「待恢复文件列表」的存放位置( M-x recover-session 读的就是它)。 auto-save-file-name-transforms 是一个 (REGEX REPLACEMENT UNIQUIFY) 三元组列表,第三个元素 t (uniquify 标志)会把原始路径编码进文件名,这样两个同名的文件就不会在 auto-save 目录里互相覆盖了。
TRAMP 等延迟加载变量
use-package emacs 的 :custom 块在启动时就执行了,但有些变量的定义来自后续才加载的包。包还没加载就去设它的变量,轻则无效,重则报错。遇到这种,把 setopt 放进对应包的 :config 块:
(use-package tramp :config (setopt tramp-persistency-file-name (my/cache--path 'tramp-persistency-file-name)))
setopt 是 setq 的现代替代品,专门用于 defcustom 定义的变量。关键区别在于: setopt 会执行变量定义的 :set 函数(如果有的话), setq 直接赋值则会跳过这一步。 tramp-persistency-file-name 定义了 :set 函数来触发 TRAMP 内部重载,用 setq 会跳过这一步,导致 TRAMP 状态不一致。
完整代码 + 最终效果
所有组件拼在一起,就是这样:
(defcustom my/cache-directory (expand-file-name "cache/" user-emacs-directory) "Base directory for Emacs cache files." :type `(choice (const :tag "Inside Emacs config (cache/ in user-emacs-directory)" ,(expand-file-name "cache/" user-emacs-directory)) (const :tag "System temp (/tmp/emacs-cache/)" "/tmp/emacs-cache/") (directory :tag "Custom directory")) :group 'my) (when custom-file (load custom-file 'noerror 'nomessage)) (defvar my/cache-paths '((bookmark-file . "bookmarks") (ielm-history-file-name . "ielm-history.eld") (project-list-file . "projects") (recentf-save-file . "recentf") (savehist-file . "history") (save-place-file . "saveplace") (transient-history-file . "transient/history.el") (transient-levels-file . "transient/levels.el") (transient-values-file . "transient/values.el") (tramp-persistency-file-name . "tramp") (nsm-settings-file . "network-security.data") (auto-saves . "auto-saves/") (auto-saves-sessions . "auto-saves/sessions/") (multisession-directory . "multisession/") (url-configuration-directory . "url/") (image-dired-dir . "image-dired/") (erc-log-channels-directory . "erc/logs/") (erc-image-cache-directory . "erc/images/") (rcirc-log-directory . "rcirc/logs/")) "Alist of (KEY . RELATIVE-PATH) for cache locations.") (defun my/cache--path (key) "Return the absolute path for KEY in `my/cache-paths'." (let ((rel (cdr (assq key my/cache-paths)))) (unless rel (error "my/cache--path: Unknown key %S" key)) (expand-file-name rel my/cache-directory))) (defun my/cache--ensure-dirs () "Create every directory referenced by `my/cache-paths'." (dolist (entry my/cache-paths) (let* ((abs (my/cache--path (car entry))) (dir (if (directory-name-p abs) abs (file-name-directory abs)))) (make-directory dir t)))) (my/cache--ensure-dirs) (use-package emacs :ensure nil :custom (bookmark-file (my/cache--path 'bookmark-file)) (ielm-history-file-name (my/cache--path 'ielm-history-file-name)) (project-list-file (my/cache--path 'project-list-file)) (recentf-save-file (my/cache--path 'recentf-save-file)) (savehist-file (my/cache--path 'savehist-file)) (save-place-file (my/cache--path 'save-place-file)) (transient-history-file (my/cache--path 'transient-history-file)) (transient-levels-file (my/cache--path 'transient-levels-file)) (transient-values-file (my/cache--path 'transient-values-file)) (nsm-settings-file (my/cache--path 'nsm-settings-file)) (multisession-directory (my/cache--path 'multisession-directory)) (url-configuration-directory (my/cache--path 'url-configuration-directory)) (create-lockfiles nil) (make-backup-files nil) (auto-save-default t) :config (setq auto-save-list-file-prefix (my/cache--path 'auto-saves-sessions) auto-save-file-name-transforms `((".*" ,(my/cache--path 'auto-saves) t)))) (use-package tramp :config (setopt tramp-persistency-file-name (my/cache--path 'tramp-persistency-file-name)))
启动后, ~/.emacs.d/ 的目录结构会变成这样:
.
├── cache
│ ├── auto-saves
│ │ └── sessions
│ ├── erc
│ │ ├── images
│ │ └── logs
│ ├── image-dired
│ ├── multisession
│ ├── rcirc
│ │ └── logs
│ ├── transient
│ │ ├── history.el
│ │ ├── levels.el
│ │ └── values.el
│ ├── url
│ ├── bookmarks
│ ├── history
│ ├── ielm-history.eld
│ ├── network-security.data
│ ├── projects
│ ├── recentf
│ ├── saveplace
│ └── tramp
├── eln-cache
│ └── 32_0_50-25c5b284
├── eshell
│ └── history
├── init.el
└── tree-sitter
├── libtree-sitter-markdown-inline.dylib
└── libtree-sitter-markdown.dylib
所有缓存都在 cache/ 下一个目录,想清就清,想留就留。 eln-cache 、 eshell 、 tree-sitter 是编译产出和 dotfile,不在 alist 管理范围内。
新包接入,两行配置
每当你开始用一个会产生缓存文件的新包,只需要两处改动:
- 在
my/cache-paths里加一条映射 - 在
use-package :custom里加一行
下次重启时,目录自动创建,变量自动指向新地址。
与 no-littering 的对比
原文提到,如果你不想自己维护这套配置,还可以用 no-littering 这个包。两种方案各有利弊:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自己维护 alist | 完全可控,零外部依赖,知道每个文件在干什么 | 新增包时需手动加映射 |
| no-littering | 开箱即用,覆盖面广,社区维护 | 多一个依赖,定制不如自己写的灵活 |
如果你的包列表稳定、不经常增减,自己维护 alist 是更轻量的选择。如果经常尝试新包、不想费心记路径变量名,no-littering 省事。