命令行工具cua:用Python实现代码扫描、目录统计与缓存清理 我本来没想过要专门写一个命令行工具直到上个月末我数了一下自己的~/scripts目录里面躺了十三个脚本有清理日志的、有统计代码量的、有批量重命名的、还有一个文件名带 fix 字样的但我已经完全不记得它当初要修什么问题。那段时间手头两个项目正好在赶进度每天重复最多的就是三件事扫代码里遗留的 TODO/FIXME 标记、查某个目录为什么突然膨胀、顺手清掉各种临时文件。于是我开始琢磨与其继续往~/.bashrc里堆 alias、在 scripts 目录里翻各种名字都记不全的脚本不如写一个名字短到根本不需要记忆的命令——它叫cua读作咔。内部全称是 Command Utility Assistant但大家更习惯叫它咔因为用起来的感觉就是咔一下出结果。这篇文章完整记录我从零到一写它的过程为什么要做、怎么从一堆想法里砍到只剩三个功能、核心实现里那些值得注意的细节以及后来实测中踩过的几组坑。1. 为什么我要给终端写一个叫 cua 的小工具1.1 脚本堆积成山之后的真实困境先说背景。这个项目不是公司立项也不是某个团队分配的任务纯粹是个人把日常操作烦到头之后的自救。天天写代码的人终端里总会有一些高频但琐碎的动作进到项目目录后想看有没有人留了 FIXME、发版前想确认哪些旧的构建缓存占地方、磁盘告警后想快速定位到底是哪个依赖目录在膨胀。这些事情系统自带命令都能做只是组合起来很痛苦。比如我想统计某个目录下面有多少 Python 文件和多少 JSON 文件可以用find加wc组合但输出格式丑得没法看想看哪个子目录体积最大可以用du -sh */再sort但大目录一多输出一堆路径还要自己数。更麻烦的是这套组合命令我两周不用就会忘下次又要翻 shell 历史。1.2 三个高频动作的共性我花了一个晚上把自己在终端里执行过的历史命令按次数排了个序发现排在最前面的操作高度集中查找代码里的 TODO/FIXME/HACK 并生成报告、查看目录体积分布和文件类型统计、清理特定类型的缓存文件。这三个动作的共同点是单步执行都很简单但每一步输出格式都不一样中间还要拼接、排序、过滤真正落地操作时往往要敲五六行命令。这时候我开始意识到我需要的不是一个更快的 grep也不是一个更好看的 du而是一个把多步操作收敛成一个固定入口的小工具。它得满足三个条件名字短到不用记、输出格式稳定到不用想、行为安全到不用怕。1.3 一个单词为什么这么重要工具的名字长度和它的使用频率其实是强相关的。ls、cd、grep这些命令为什么天天用因为它们短。终端里的输入成本是很实在的——一个命令如果四个字母以上还要带一堆参数我大概率会犹豫要不要直接手写循环。给工具起名cua的时候我其实是想要那种敲下去不需要停顿的感觉就像按一次快捷键。这个思路后来也被验证了。我用了一段时间cua之后发现自己写临时脚本的频率明显变低了。以前遇到重复三次的操作第一反应是写个脚本吧现在第一反应是这能不能用 cua 做因为用现成的收敛入口比造一个新脚本要快得多。2. 功能收敛比功能堆砌难得多从 13 个想法砍到 3 条命令2.1 最初的欲望清单新手做工具最容易犯的毛病是贪多我也不例外。第一版设计稿列了 13 个功能批量重命名、重复文件查找、端口占用检查、批量压缩、编码探测、git 历史清理、日志轮转、缓存清理、TODO 扫描、目录体积统计、大文件定位、文件类型分布、构建产物清理。每个功能我当时都能说出一个真的很需要的日常场景。功能多本身不是问题问题在于每多一个功能命令的 help 信息就多一段参数就多几个出 bug 的面就大一圈。一个定位是个人效率工具的命令如果使用前还得看一遍文档它的价值就已经减半了。2.2 三条筛选标准我给自己定下来三条近乎苛刻的保留标准每周至少用五次少一次就砍掉说明还没痛到需要为它做一个命令。能不能用一行系统命令替代如果能就不进cua。参数不能超过一个超过一个就说明这个功能还不够收敛。按这个标准过了一遍13 个想法淘汰到只剩 3 个。批量重命名我用rename加正则就够了端口占用检查一条lsof -i搞定大文件定位find -size 100M也很好用。真正留下来的是scan、usage、clean三条子命令。2.3 为什么现成命令的组合替代不了有人可能会说TODO 扫描用grep -rn TODO不就行了目录统计用du -sh不就行了单看确实行。但实际场景里我要的并不是把含 TODO 的行打出来而是给我一份按目录归类的报告去掉第三方依赖目录只看业务代码带上最后修改时间和优先级关键字。这个逻辑用纯 shell 写当然也能写但写出来基本就是一段几十行的 awk 加 sed 大杂烩换台机器就得重新调试。cua的价值不是检测而是把复杂的前置条件和后续处理都封装起来让使用者只记一个单词。scan、usage、clean的输出格式是完全统一的每个命令最多接受一个路径参数没有需要翻文档才能理解的开关。3. 核心实现里的几个细节以及每个选择背后的理由3.1 为什么用 Python 而不是 Shell 或 Go做这个选择时我很明确工具面向的是人的效率不是机器的极致性能。Shell 脚本写起来是快但三个命令放到一起的规模肯定会超过五百行控制复杂度就比较吃力Go 和 Rust 编译出来确实快但为了一个个人使用的工具起一个完整的编译项目我感觉维护成本偏高了。最后落到了 Python 3 标准库全程零第三方依赖核心思路是启动速度可以接受实测大约 150ms 左右但代码必须以可读为第一优先级。用 Python 还有个附带好处三个子命令可以放在同一个项目里共用路径处理和输出格式化的代码。入口文件只负责参数分发实际逻辑按子命令拆到commands/目录。项目结构长这样cua/ ├── cli.py # 入口解析子命令并分发 ├── commands/ │ ├── __init__.py │ ├── scan.py # scan扫描 TODO/FIXME 等标记 │ ├── usage.py # usage目录体积与文件类型分布 │ └── clean.py # clean按白名单清理缓存 └── install.sh # 安装脚本软链到 ~/.local/bininstall.sh做的事情很简单把cli.py软链到~/.local/bin/cua这样任何目录下都能直接敲cua不需要考虑 PYTHONPATH。我在脚本里加了 shebang配合可执行权限整体体验和原生二进制没什么区别。3.2scan子命令真正的难点是区分源码和噪音scan的核心逻辑是递归扫描目录按正则找出 TODO、FIXME、HACK 标记然后输出一个带文件路径、行号、内容和优先级的报告。听起来简单但处理真实项目时有几个很实际的问题第一二进制文件不能碰。如果扫描函数对每个文件都用文本方式读一遍遇到图片、打包产物会直接报编码错误也可能拖慢整个过程。我的做法是先做一次文件大小判断超过 10MB 的直接跳过然后以二进制模式读文件头检测内容里是否包含大量空字节来判断是不是文本文件。第二依赖目录要跳过。扫描node_modules、.git、dist这类目录没有意义绝大多数情况下还特别慢。我在实现里维护了一个跳过目录名单同时也支持用户在家目录下放一个配置文件按项目类型追加需要忽略的目录。第三输出格式要稳定。scan的结果最后可以输出成两种形态终端直接看到的缩略列表以及一份 Markdown 格式的待办清单。这样我可以直接开源到团队内部别人拿去跑一下也能直接看懂。3.3usage子命令体积统计里藏着文件系统的小陷阱usage的功能是递归统计每个子目录的体积同时按扩展名做归类直接给出 top 15 的目录和 top 10 的文件类型。用 Python 实现时最核心的循环其实很朴素def scan_volume(root): total 0 ext_map {} dir_map {} for path in Path(root).rglob(*): try: if path.is_symlink(): continue if not path.is_file(): continue size path.stat().st_size total size ext path.suffix.lower() or [noext] ext_map[ext] ext_map.get(ext, 0) size parent str(path.parent) dir_map[parent] dir_map.get(parent, 0) size except OSError: continue return total, dir_map, ext_map这段代码里有一个关键的细节path.is_symlink()必须放在path.is_file()之前。Python 的Path.is_file()默认会跟随符号链接如果不先排除你统计出来的体积会把链接指向的文件也重复算一次而且当链接出现环时可能直接卡死在遍历里。还有一个细节是关于st_size的它统计的是表的文件大小不是磁盘占用。对于普通场景没问题但如果你要关心那个用了大量小文件导致块开销很大的目录这里的数字会偏小。我在输出里加了一行提示说清楚这是文件总大小而不是磁盘占用避免误读。3.4clean子命令安全边界怎么画才靠谱clean是最危险的一个子命令因为它要删东西。第一版实现里我踩过误删的坑后面有详细排查过程所以在最终版里设计了一套强制安全规则只允许清理预置白名单里的路径模式比如~/Library/Caches/*/xxx-tool/任何其他模式一律不在默认集合内。删除前必须确认目标目录存在且满足三层校验真实路径前缀匹配、路径深度不低于三层、预估占用不超过某个上限。默认不直接删除文件而是先按体积排序预览让你看到要清理的内容后再加--confirm真正执行。我把安全默认当成设计底线。与其冒失地做一个快但可能出事的工具不如故意做得慢一点、谨慎一点。对一个个人效率工具来说出一次事故要损失的信任成本远比省下的那两秒大。4. 踩坑实录四段花费一整晚的排查经历4.1scan偶发内存暴涨一次性读文件的代价用了一阵子之后我发现在某些项目目录上跑scan会偶发内存升高而且总是在脚本快结束时才出现一开始很难定位。第一反应是正则回溯的问题但把正则简化后问题还在。后来我用 htop 盯着一颗比较大的模拟项目跑发现内存曲线是突然在某个阶段涨上去的——问题不在正则而在文件读取方式。原因找到了早期实现的scan会用path.read_text()读整个文件一个几百 MB 的自动生成日志被当作文本文件整个读进内存内存自然爆掉。修复方式很简单把整文件读取改成逐行读取超过 10MB 的文件直接标记为超大文件已跳过不进扫描范围。这个排查过程有几个值得记录的地方不要一上来就怀疑最复杂的环节先看数据内存问题尽量不要靠猜开着资源监视器复现一次就清晰了。4.2usage在符号链接目录上卡死rglob隐藏的死循环usage上线后没几天我跑一个大型项目时发现它 30 秒都没结束。当时第一反应是目录太大顺手Ctrl-C看了一眼调用栈——好家伙卡在某一次递归遍历里不动了。根因是符号链接环项目里有几个链接指向了项目根目录的上级再往上一级又有链接指回自身形成了一个环。我最初的实现用的Path.rglob(*)默认情况下它还是会一路跟下去。修复方案很干脆遍历时统一走os.scandir配合手动记录已经访问过的目录 inode遇到重复就直接剪枝。现在即使在诡异的符号链接结构下usage也最多会多花一点时间扫一遍不会再卡死。4.3clean差点删错位置字符串拼接路径的教训这个坑是我最后怕的一个好在发生在模拟环境里。早期clean实现清缓存时路径拼接我用的是字符串而不是os.path.join。某次测试时我把清理目标的根路径写成了~/tmp-test却在清理规则里拼出了~/tmp-test2两条路径在外表上看起来差不多结果clean把~/tmp-test2里的一堆内容删空了。当时环境里全是模拟文件影响不大但整个过程让我出了一身冷汗。修复不只是换一个函数我把所有路径操作全部切到了pathlib同时给删除动作加了三道防线——真实路径前缀必须精确匹配、目标路径层级严格校验、删除前先输出全部文件列表。从那以后我再也没有在clean上出过事故。这里最想提醒的是写任何带删除能力的工具路径处理都不能图省事用拼字符串这一点在跨平台上尤其重要。4.4 跨平台编码问题Windows 日志和 macOS 路径同时出事我在类 Unix 环境里开发的后来有一版给一个用 Windows 的同事试了一下在读取一份 GBK 编码的日志时直接抛了UnicodeDecodeError。排查时发现两个独立的根因第一编码问题。在 Windows 上很多日志文件用的不是 UTF-8。我的修复方式是在读取文本时同时尝试 UTF-8 和系统默认编码都失败就带上errorsreplace兜底同时声明该文件有编码替换风险。第二路径分隔符问题。虽然pathlib已经处理掉大部分路径差异但我发现自己在字符串格式化时还是有一两处 / 拼接这在类 Unix 上正常到了 Windows 上就会产生假路径。最终统一改成f{path}或者直接用Path对象格式化。我把这四段经历整理成了一个小表方便自己以后自查现象根因修复scan 内存偶发暴涨整文件读入内存逐行读取 大小上限usage 某些目录卡死符号链接成环记录 inode 并剪枝clean 删错位置字符串拼接路径pathlib 三重校验Windows 上编码报错非 UTF-8 日志多编码尝试 替换兜底4.5 每改一个 bug都要拿真实目录做回归这些坑里最核心的教训是一个工具的价值不仅在于功能更在于你改完一个 bug 之后原来能跑的场景没有被影响。我后来整理了一个小的 fixture 集合里面有一个带着符号链接环的项目结构、一个混着 GBK 和 UTF-8 文件的目录、一个包含超大文件的目录每次改动代码后都会把这三个场景全部跑一遍。这套回归流程虽然简单但帮我挡掉了至少两次新引入的问题。5. 实测数据与使用习惯快不是唯一标准顺手才是5.1 三个子命令的实际耗时为了确认这个工具没有白做我在一个大约有 3000 个文件、合计 180MB 左右的模拟项目目录上做了一轮实测。环境是普通笔记本冷缓存状态命令耗时内存增量cua usage约 1.2s约 40MBcua scan约 3.8s约 60MBcua clean约 2.0s约 25MB作为对比用系统自带的du -sh看总大小只需 0.05s但它只给一个数字要看按目录分布还得配合find加awk整体算下来也要 2 秒以上输出格式还要自己调。scan和直接grep的对比也是一样的道理grep找得快但把结果整理成按文件分组的 Markdown 格式报告处理起来同样不省事。这些数据说明一个点对这类工具来说真正的时间瓶颈不在命令本身花掉的那几秒钟而在于拼接多条命令、排列输出、记忆语法时消耗的心智成本。cua宁可多花零点几秒也要让输出在一眼内可读。5.2 使用半年后的真实验感我现在基本养成了三个固定习惯每次开始新功能前跑一次cua scan确认没有遗留的 TODO 和 FIXME每周下班前在项目目录下跑一次cua usage看看哪个目录在偷偷变大及时把不需要的依赖清理掉每月用cua clean清一轮开发工具缓存。三个命令加起来每天消耗的时间不超过两分钟但它帮我挡住过至少两次磁盘满了才发现的尴尬。如果你也有一个塞满零散脚本的目录、一堆自己想不起来作用的 alias我的建议是别急着再写新脚本先花一个晚上统计自己的高频动作砍掉那些低频需求然后把剩下的三五个动作做成一个名字短到不用记的收敛入口。做完之后你会发现工具本身写得好不好还在其次给自己的终端减轻记忆负担这个习惯才是真正值钱的东西。