一个敢删你 95GB 文件的终端命令,最值钱的代码却在教它别动手
- 分层设计将 Shell 调度与 Go 计算分离,保留删除逻辑的审计入口
- 五把信号量按资源画像分别限流,防止并发扫描性能崩溃
- 缓存准入制度限制工具自身膨胀,避免成为新的磁盘杀手
- 多层安全防线包括 dry-run、CACHEDIR.TAG 验签和 root 权限隔离
- 健康分模型修正了数学 bug,体现工程严谨性和自省态度
一个会动手删你文件的命令行工具,你敢装吗?
说真的,大多数人听到“自动清理 Mac”这四个字,第一反应不是期待,是慌。CleanMyMac 这类工具火了很多年,口碑一直两极分化,一边是“真省心”,一边是“把我系统搞崩过”。所以当我看到 tw93 的 Mole,一个用终端命令 mo clean 号称能给你腾出 95GB 空间的开源项目,半年里冲到 6 万多 Star。说实话,我第一反应没去数星标,脑子里只转着一件事。
它凭什么敢碰我的磁盘。
扒完源码我发现一件挺反直觉的事。这个项目里真正值钱的代码,几乎全在教它“别动手”。删东西的逻辑写起来不难,难的是怎么删得让你放心。这篇文章我想拆开讲讲,Mole 是怎么把“破坏性操作”做成一件可信任的事的。
它到底是个什么东西
一句话定位,Mole 是一个 Mac 维护工具箱,把清理、卸载、磁盘分析、系统监控、缓存优化五件事,塞进了 mo 这一条命令里。
作者是 tw93,独立开发者圈子里很熟的名字,之前做过 Pake(把网页包成 Mac 应用)和 Kaku(终端)。Mole 从 2025 年 9 月开始,到现在一年不到,版本号已经滚到 V1.50.0,基本三四天一个 release。63347 个 Star,2229 个 Fork,GPL-3.0 开源,同时有一个叫 Mole for Mac 的付费 GUI 版挂在 mole.fit 上卖。
你可以 brew install mole 装上,然后敲 mo 进交互菜单。常用就这几条。
mo clean # 深度清理,清缓存、日志、已卸载应用的残留
mo uninstall # 连应用带残留一起卸
mo optimize # 刷新缓存和服务
mo analyze # 可视化磁盘分析
mo status # 实时系统状态面板
mo purge # 清项目构建产物(node_modules、target 那些)
到这里都是产品介绍,真正有意思的从代码结构开始。
Shell 搭 Go,为什么是混血
很多人第一眼会把它当成一个纯 Shell 脚本项目,毕竟 GitHub 上标的 primary language 就是 Shell。但你要是真 clone 下来看目录,会发现它是两层。
外层是 bin/ 下一堆 .sh 文件,clean.sh、uninstall.sh、optimize.sh、purge.sh,负责调度和那些“删文件”的危险活。内层是 cmd/ 下两个 Go 程序,cmd/analyze 和 cmd/status,干的是磁盘扫描和系统监控这种计算密集的活。
这个拆法是有讲究的。Shell 写清理逻辑,好处是好读、好改、用户能直接看懂它要删什么,出问题也好审。但你要用纯 Shell 去扫一个几十万文件的磁盘目录,或者实时算 CPU 负载和健康分,性能会被按在地上摩擦。于是重的计算交给 Go 编译成原生二进制,轻的调度留在 Shell。
你想想看,它不图省事用 Shell,也不追求纯粹用 Go。它是按活的风险和算力需求,从中间切了一刀。删文件的逻辑暴露在 Shell 里,等于天然给用户留了个审计口。这个设计判断,我个人觉得比单纯用什么语言重要得多。
扫磁盘这件事,被它做成了五把锁
mo analyze 那个可视化磁盘分析,背后是 cmd/analyze/scanner.go。这是整个项目里我最想聊的一段代码。
扫描磁盘目录听起来简单,递归遍历累加大小就完了。但真实场景是,你家目录下几万甚至几十万个文件,同时跑会怎样?CPU 打满、磁盘 I/O 排队、内存被无数个等待的 goroutine 撑爆。tw93 的解法是在 scanner.go 里写了一个叫 scanLimiter 的结构,里面塞了五把独立的信号量。
type scanLimiter struct {
entrySem chan struct{} // 顶层入口并发数
dirSem chan struct{} // 递归目录遍历并发数
duSem chan struct{} // du 子进程并发数
duQueueSem chan struct{} // 排队等 du 的 goroutine 上限
fastSem chan struct{} // du 不可用时的兜底遍历并发数
seen sync.Map // 硬链接去重(dev, ino)
}
为什么是五把而不是一把?源码注释里有一句原话讲得很直白,说每一把保护的是“不同的稀缺资源”,把它们合并会改变扩展行为,而且很容易改错。
我挑两个有意思的说。duSem 控制的是同时能跑多少个 du 子进程,被刻意压到很低,上限是 min(4, NumCPU)。原因是每个 du 进程本身就在重度并行打磁盘,你放开了跑,磁盘一饱和,整体反而更慢。duQueueSem 更妙,它限的不是在跑的,而是“排队等着跑”的 goroutine 数量,设成 duSem 的两倍。注释解释说,要是不卡这个上限,一个大的家目录会为每个待扫目录起一个 goroutine,内存随输入规模线性涨。两倍这个数刚好能让 worker 侧不闲着,又不至于无限堆。
这其实就是典型的“按资源画像分别限流”。我见过太多并发代码,上来一个 worker pool 通吃所有任务,跑小数据没事,跑大数据要么 OOM 要么把磁盘打趴。Mole 这五把锁的核心思路,是把“入口数、遍历数、子进程数、排队数”当成四种不同的瓶颈分别治理。这个判断可以迁移到任何需要并发扫文件树的场景。
顺带,它用 sync.Map 存 (dev, ino) 做硬链接去重,保证一个有多条硬链接的文件只算一次,行为对齐系统的 du。还有算 Top N 大文件那段,heap.go 里用了 entryHeap 和 largeFileHeap 两个最小堆,只要 Top N 就不用把全部结果排序,堆的大小始终卡在 N。这种细节单独看不稀奇,叠在一起就能解释为什么它扫一个 75GB 的家目录,你感觉不到卡。
一个健康分,藏着一个被修掉的数学 bug
mo status 那个实时面板右上角有个健康分,0 到 100,比如 92。这数字怎么来的,我翻了 cmd/status/metrics_health.go。
它是个加权扣分模型,满分 100,按权重往下扣。CPU 占 30 分,内存 25,磁盘 20,温度 15,磁盘 IO 10。每个指标超过阈值就分段扣分。
const (
healthCPUWeight = 30.0
healthMemWeight = 25.0
healthDiskWeight = 20.0
healthThermalWeight = 15.0
healthIOWeight = 10.0
cpuNormalThreshold = 50.0
cpuHighThreshold = 85.0
)
但真正吸引我的是代码里的一段注释。CPU 用量超过 85% 时,扣分公式是 healthCPUWeight * (usage - normalThreshold) / (100 - normalThreshold)。注释说,他们一开始除的不是 (100 - normalThreshold),而是直接除以 cpuHighThreshold。结果出了个反直觉的 bug,负载超过 85% 之后,扣分反而开始往下掉,健康分随着 CPU 越来越高不降反升。
这是一个真实的、被注释记录下来的修复。换成除以 (100 - normalThreshold) 之后,惩罚值会随着负载一路涨到 100%,不会掉头。内存那条分支的注释也是一模一样的故事。
这种东西你在 README 里永远看不到。但它恰恰说明一个问题,一个看着简单的“健康分”,要做到数学上自洽、行为符合直觉,坑是实打实踩过的。这也是我为什么坚持要读源码而不是只看介绍,因为修过的 bug 才能告诉你,这个团队到底在意什么。
清理工具自己攒了 7.8GB 缓存,这个坑最有意思
接下来这段是我觉得整个项目最“诚实”的地方,也是它最值得讲的批判证据。
mo analyze 为了加速重复扫描,会把扫过的目录大小缓存下来。这个缓存本身的设计,藏在一个叫 constants.go 的文件里。读这段注释的时候我差点笑出来。
注释讲了一个真实事故。在一个开发者的机器上,缓存目录最终堆到了 188 万个文件、7.82GB。原因是什么呢?他们分析了一下那个 15.7 万条目的缓存样本,发现中位数那条目描述的是一个“只装了一个文件的目录”,98% 的条目装的文件不到 100 个。也就是说,几乎所有缓存文件,都在用一个 4KB 的 APFS 块加一个 inode,去记住“一次 readdir 能返回什么”。
一个帮你清磁盘的工具,自己悄悄攒了 7.8GB 的缓存。这个反差本身就够写一段。但更值得看的是它怎么填这个坑。tw93 没有简单加个“清理缓存”的按钮,而是给缓存加了一道准入制度。
subdirCacheMinFiles = 100 // 子目录文件数少于 100 不值得缓存
subdirCacheMinSize = 10 << 20 // 小于 10MB 不缓存
analyzerCacheMaxEntries = 5000 // 最多 5000 条
analyzerCacheMaxBytes = 50 << 20 // 总量上限 50MB
只有当一个子目录“重新扫它真的费劲”时才让它进缓存,门槛是 100 个文件或 10MB。再配上总量上限 5000 条、50MB 的退路。用他们注释的话说,这套阈值留下了那大约 1.5% 真正有复用价值的条目。
我一直觉得,衡量一个工具有没有认真做工程,不是看它加了什么功能,而是看它怎么处理自己制造的烂摊子。很多清理类工具,自己就是最大的磁盘杀手,只是用户不知道。Mole 把这个事故写进源码注释,然后用准入制而不是清理脚本去解决,这个取舍是有水平的。
它的克制,是一整套系统工程
把上面几段串起来看,你会发现 Mole 的安全设计不是某一个开关,而是一层套一层。
先说删文件的确认机制。所有破坏性命令都默认带 --dry-run,你可以先预览它要删什么,再决定动不动。操作日志写到 ~/Library/Logs/mole/operations.log,可以用 mo history 回看。
再说它对“什么能删”的判断。cmd/analyze/cleanable.go 里有一个 hasValidCacheDirTag 函数,它认一个叫 CACHEDIR.TAG 的标记文件。这个标记是备份工具业界的一个约定,有固定签名。
const cacheDirTagSignature = "Signature: 8a477f597d28d172789f06886806bc55"
它的判断不是“这个文件存在就行”,而是读出来逐字节比对签名。这样能防止某个目录里恰好有个同名文件就被误判成缓存给删了。这种“验签而非认名”的细节,一不留神就会偷懒,它没偷。
还有一个我特别欣赏的防线。bin/clean.sh 里维护了一个 PROTECTED_SW_DOMAINS 数组,把 github.com、docs.google.com、codepen.io 这类站点标记为受保护域名,清浏览器缓存时绕开它们的离线数据。你想想看,一个清理工具主动保护你的 GitHub 和 Google Docs 离线副本,这是它替你想到了你不会想到的坑。
最让我意外的是它对 root 权限的处理。清理命令经常需要 sudo,但预览文件却不能以 root 身份去写。注释里提到一个真实 issue #1210,说 root 跑的 dry-run 会把预览内容写进一个 root 拥有的文件,再通过一个普通用户进程发布出去。如果这中间文件是用户可控的符号链接,就有可能被 root 打开写入,这是个经典的提权路径。它的解法是,用 get_invoking_home 拿到“真正发起命令的那个用户”的家目录,而不是直接用 $HOME 或 root 的家。也就是说,即便整个命令跑在 root 下,白名单和预览文件始终归属发起用户。
MOLE_USER_HOME="$(get_invoking_home)"
一个 Mac 清理脚本,在防符号链接提权。我坦白讲,这是我见过的同类工具里安全意识最细的一个。
当然,它不是银弹
说完好的,该说不好的了。我前面留了个钩子,这工具确实出过事。
翻 issue 区,有一条编号 #136 的,标题是 [BUG] System Settings is corrupted after mo optimize,22 条评论。说的是 mo optimize 这条命令,在某个版本把用户的系统设置搞崩了。optimize 是用来刷新缓存和系统服务的,本身就是个风险偏高的操作,这事儿的真实发生,说明它离“绝对安全”还很远。这也是为什么后来版本里 optimize 的描述变成“跳过不必要、当前不安全或不可用的任务”,能感觉出是被教训过之后改的措辞。
再说说维护风险。我把贡献者拉出来看了一眼,tw93 一个人 2244 次提交,排第二的是个 bot(github-actions 221 次),排第三的真人 youxi798 是 91 次。这是个典型的 bus factor 等于 1 的项目。tw93 是它的灵魂,也几乎是它的全部。一年不到 50 个版本的高强度迭代,很猛,但也意味着一旦他停下来,这个项目就有断档风险。这是个事实,不是缺点,只是你在重度依赖之前要想清楚。
还有商业模式这块。CLI 是 GPL-3.0 开源,但 Mole for Mac 那个 GUI 版是闭源商业软件,一份授权覆盖两台机器、终身更新、14 天退款。这种“CLI 免费养口碑、GUI 收费养开发”的双轨,现在越来越常见,我觉得挺健康。只是要提醒一句,GPL-3.0 是个有传染性的协议,你要是基于 Mole 二次开发做产品,得保持同样的开源协议,README 里也明确说了 fork 出去得改名字并注明来源。
另外 Windows 支持还在一个单独的 windows 分支上,官方说法是 experimental,早期尝鲜用。主力还是 macOS 14 及以上,老系统只能走脚本安装,属于 best-effort。
一个可以带走的判断
拆到最后,我想提炼一个东西出来。
Mole 给我最大的启发,跟它用 Shell 还是 Go 没关系,跟那五把信号量也没关系。它是一种工程取向,我管它叫可验证的克制。
一个工具的核心动作是“删除”,但它的工程价值,几乎全部体现在“不删”上。dry-run 让你能预览,操作日志让你能回溯,CACHEDIR.TAG 验签让判断可信,受保护域名列表替你兜底,root 权限隔离防提权,缓存准入制连它自己制造的副作用都管住。这些加起来,才是一个破坏性工具值得被信任的理由。
这个判断是可以迁移的。不管你在做的是清理工具、批量删除脚本、数据库迁移、还是任何“会改用户数据”的自动化,值得投入的工程力气,永远应该往“可验证的克制”那侧倾斜。把“它没做什么”做得可见、可审、可回滚,比把它“能做什么”做得更猛要难得多,也值钱得多。
说到底,一个敢碰你磁盘的命令,它让你放心的地方,恰恰是它为自己设下的那些限制。
如果你用 Mac,值得 brew install mole 装一个,先跑 mo status 和 mo analyze --dry-run 试试水。真要清理之前,养成先 --dry-run 的习惯。这个习惯不是 Mole 教你的,是所有会动你数据的工具,都该让你养成的。

评论互动