undo:用 LD_PRELOAD 钩子给每个 shell 命令装上后悔药
项目速览:撤消最后一个 shell 命令对文件系统所做的操作。钩。 shell 钩子(zsh、bash、fish)围绕您运行的每个命令配备一个小型 LD_PRELOAD 库 libundo.so。
秒懂
- 它是什么?
- undo 是一个 shell 钩子项目,通过 LD_PRELOAD 拦截 libc 调用,记录每个命令对文件系统的改动,让你能撤销最近一条命令的破坏。它不依赖快照,不需要 root,但也有一些明显的边界。
- 适合谁用?
- 如果你经常在 shell 里执行 rm、mv、重定向或 chmod,而且不想改变习惯去用 trash 命令或文件系统快照,undo 值得一试。它适合个人开发机、CI 之外的交互式终端场景,尤其是构建脚本里深层进程的误删。
- 能商用吗?
- 可以。MIT 是宽松许可证:你可以使用、修改并销售基于它的软件,只需保留版权和许可证声明。
- 还在维护吗?
- 在维护。仓库最近一次提交在 35 天前。
- 用什么语言写的?
- 主要是 Go(依据 GitHub 的语言统计)。
以上回答依据项目的 GitHub 数据(最近同步于 2026年9月15日)和我们的分析,不构成法律意见。
开源项目深度解析
shell 没有撤销,undo 想补上这个洞
shell 历史上就没有撤销机制。常见的建议是让你先改习惯:把 rm 别名成 trash,或者提前配置 btrfs/zfs 快照。undo 反其道而行,它不要求你改变任何输入方式。你照常敲 `rm -rf`,它在你背后记录这次命令动了哪些文件。关键区别在于它是命令粒度的,不是时间粒度的。快照会把整个文件系统回滚到某个时间点,连带你那次命令之后做的好改动一起丢掉。undo 只回滚那一条命令触及的内容。它面向的是那些经典失误:`rm` 删错文件、`mv` 覆盖了还需要的文件、`>` 截断了内容、`chmod -R` 改错权限、脚本批量创建了多余文件。这些场景里,别名和快照要么覆盖不到,要么代价太大。
LD_PRELOAD 钩子加硬链接,不复制数据
undo 的实现没有守护进程,没有快照,也没有在命令之间跑任何东西。它的核心是一个 shell 钩子(支持 zsh、bash、fish),在每条命令执行前设置 `LD_PRELOAD` 环境变量,加载一个叫 `libundo.so` 的小型库。这个库拦截 libc 里的破坏性调用:`unlink`、`rename`、带写标志的 `open`、`rmdir`、`mkdir`、`chmod` 等。每次调用真正执行前,它先把受影响文件保存到当前会话目录,同时往 journal 文件追加一行记录。删除操作通过硬链接保存,所以 `rm -rf` 几个 GB 的目录几乎不复制数据,代价很低。每个改动过文件的命令会生成一个会话目录,里面存有命令行文本、journal、pid 和 done 标记,以及 data 子目录里的硬链接备份。undo 读取最后一个会话的 journal,按逆序重放:恢复删除的文件、把 rename 移回去、用备份换回被截断的文件、移除意外创建的内容。
安装三步走:二进制、钩子、doctor 验证
安装路径有几种,最简单的是官方一行脚本:`curl -fsSL https://undo.edaywalid.com/install.sh | sh`。它安装到 `~/.local`,然后会询问是否往 shell rc 文件里加钩子行,并且先展示具体那行内容。如果回答否,它会打印手动添加的说明。在 CI 或管道环境里没有终端可交互,它不会自动编辑任何文件。脚本安装可以用 `UNDO_MODIFY_RC=1` 或 `UNDO_NO_MODIFY_RC=1` 预设答案。包管理器渠道包括 Homebrew(Linux)、Debian/Ubuntu 的 .deb、Fedora/openSUSE 的 .rpm、Arch 的 AUR 包、Nix flake。从源码编译需要 gcc 和 go,运行 `make install`。装完后,如果走包管理器,需要手动添加钩子行,例如 bash 5 或更新版本:`echo 'source /usr/share/undo/undo.bash' >> ~/.bashrc && exec bash`。注意 Homebrew 安装时钩子路径在 `$(brew --prefix)/share/undo/`,写 rc 时要用双引号展开,否则每次启动 shell 都会调用 brew,那是个 Ruby 程序,会很慢。最后运行 `undo doctor`,它会实际删除并恢复一个 canary 文件来验证安装是否真的有效,而不是只检查文件存在。
用法:单独一行跑 undo,别用 && 链
基本用法是 `undo` 单独一行,不带参数,它回滚最近一条改动过文件的命令。`undo -i` 可以交互式选择会话并挑选个别条目恢复。`undo redo [id]` 重新应用一个已撤销的会话。`undo diff [id]` 显示会话改动的内容差异。`undo list` 列出最近的会话,`undo show [id]` 查看具体改动。README 特别强调:`undo` 必须单独一行运行,不能和 `&&` 链在一起。因为链式命令对 shell 来说是一个整体,如果写成 `rm x && undo`,undo 会在它自己试图回滚的那个会话内部运行,而此时命令还没结束,会出问题。对于没有钩子的场景,比如脚本或 CI,可以用 `undo run -- ./sketchy-cleanup.sh` 给单条命令武装 shim,无需任何预先配置。这个设计让 undo 既能交互使用,也能嵌入到自动化流程里。
存储预算:30 个会话,1 GiB 上限,可配置
undo 的存储格式是纯文件,放在 `~/.local/share/undo/sessions/`,权限 0700。每个会话目录包含 cmd 文件(记录命令行,供 `undo list` 显示)、journal(每行一个改动)、pid 和 done 标记(防止 undo 触碰正在运行的命令)、budget 文件(跟踪已存储字节数,由会话内所有进程共享),以及 data 子目录里的硬链接。默认保留最近 30 个会话,总预算 1 GiB,两者都可配置。`undo purge` 可以清空存储。值得注意的是,最近一个会话总是保留,即使它单独就超过预算,因为那是 `undo` 要回滚的对象。这意味着如果你一次删了超大文件,存储占用可能瞬间超过 1 GiB,磁盘空间会被吃掉。硬链接虽然不复制数据,但会占用目录项,而且如果原文件被修改,硬链接会跟着变,所以备份的完整性依赖于文件未被后续命令改动。
它抓不到什么:网络文件系统、mmap、musl 和 macOS
README 明确列出了一些无法捕获的场景。首先,它只拦截 libc 调用,所以如果程序使用 `mmap` 直接修改文件内容,undo 看不到。其次,网络文件系统(NFS、SMB 等)上的操作可能无法可靠记录,因为硬链接语义和本地文件系统不同。平台支持方面,它要求 Linux amd64 或 arm64,glibc 2.6 或更新版本。musl libc(Alpine 等发行版)不支持,macOS 和 WSL 也不支持。这意味着使用这些环境的用户直接排除。另外,`chmod -R` 这种递归操作虽然会被记录,但恢复时可能无法精确还原所有目录的原始模式,因为 journal 只记录每个文件的 mode 变化,而目录本身可能被重建。还有一个重要警告:如果你用 `shred` 安全删除文件,undo 的硬链接备份可能无法恢复数据,因为 shred 会覆写文件内容,硬链接指向的 inode 内容已经被破坏。README 专门有一节叫「Secure deletion」,提醒用户先读。
对比:命令粒度 vs 时间粒度快照
undo 的替代方案主要是文件系统快照(btrfs/zfs)和 trash 别名。快照是时间粒度的,它把整个文件系统回滚到某个时间点,代价是你会丢失那个时间点之后的所有改动,包括你不想丢的。undo 是命令粒度的,只回滚那一条命令,不影响其他工作。但快照能捕获所有类型的改动,包括 mmap 写入和网络文件系统操作,而 undo 不能。trash 别名需要你改变习惯,而且只覆盖 `rm` 命令,对 `mv` 覆盖、重定向截断、`chmod` 这类操作无能为力。undo 的独特之处在于它通过 LD_PRELOAD 深入进程内部,即使删除发生在构建工具的三层子进程里,别名根本够不着的地方,它也能记录。这是它的核心价值,也是它比快照更精准的原因。不过,快照不需要预加载库,对性能影响更小,而 LD_PRELOAD 会注入到每个命令的进程里,可能带来微小开销,README 没有给出具体数字。
维护成本:升级、卸载和许可证
undo 采用 MIT 许可证,可以自由使用和修改。项目最近发布了 v0.3.0,看起来还在活跃开发中。升级方面,README 有专门的 Upgrading 章节,但内容被截断了,没有具体说明升级步骤。卸载也有一节,同样被截断。不过从安装方式推断,包管理器安装的可以用对应包管理器卸载,脚本安装的可能需要手动删除 `~/.local` 下的二进制和钩子文件。存储目录 `~/.local/share/undo/sessions/` 需要定期检查,因为 1 GiB 预算可能被单个大文件突破,而且会话文件是纯文本,可能包含敏感文件名。由于它通过 LD_PRELOAD 拦截系统调用,如果 libundo.so 有 bug,可能影响所有命令的稳定性,但 README 没有提及任何已知问题。对于生产环境,你需要自己评估这种注入方式的风险。
编辑结论
如果你经常在 shell 里执行 rm、mv、重定向或 chmod,而且不想改变习惯去用 trash 命令或文件系统快照,undo 值得一试。它适合个人开发机、CI 之外的交互式终端场景,尤其是构建脚本里深层进程的误删。但如果你依赖 musl libc、macOS 或 WSL,或者需要撤销对网络文件系统、内存映射文件的改动,它目前不是合适工具。安装后先跑 `undo doctor` 验证钩子是否生效,然后故意删除一个测试文件试试 `undo`。日常使用前,确认你的 shell 版本满足要求(bash 5+、fish 3.4+),并检查 `~/.local/share/undo/sessions` 的磁盘占用,因为 1 GiB 预算可能被单个大文件快速耗尽。
社区笔记