news 2026/10/10 10:24:10

命令行工具cua:用Python实现代码扫描、目录统计与缓存清理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
命令行工具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 三条筛选标准

我给自己定下来三条近乎苛刻的保留标准:

  1. 每周至少用五次,少一次就砍掉,说明还没痛到需要为它做一个命令。
  2. 能不能用一行系统命令替代?如果能,就不进cua。
  3. 参数不能超过一个,超过一个就说明这个功能还不够收敛。

按这个标准过了一遍,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/bin

install.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是最危险的一个子命令,因为它要删东西。第一版实现里我踩过误删的坑(后面有详细排查过程),所以在最终版里设计了一套强制安全规则:

  1. 只允许清理预置白名单里的路径模式,比如~/Library/Caches/*/xxx-tool/,任何其他模式一律不在默认集合内。
  2. 删除前必须确认目标目录存在且满足三层校验:真实路径前缀匹配、路径深度不低于三层、预估占用不超过某个上限。
  3. 默认不直接删除文件,而是先按体积排序预览,让你看到要清理的内容后再加--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 和系统默认编码,都失败就带上errors='replace'兜底,同时声明该文件有编码替换风险。第二,路径分隔符问题。虽然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约 40MB
cua scan约 3.8s约 60MB
cua 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,我的建议是别急着再写新脚本,先花一个晚上统计自己的高频动作,砍掉那些低频需求,然后把剩下的三五个动作做成一个"名字短到不用记"的收敛入口。做完之后你会发现,工具本身写得好不好还在其次,"给自己的终端减轻记忆负担"这个习惯,才是真正值钱的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 10:23:49

工业能源监测系统源码实战:从电费单数据到高耗能定位与节能降费

从一张电费单开始讲吧。去年一个朋友找到我,说厂里一个季度被供电部门加收了将近20万,理由是“超需量”和“功率因数不达标”。他手里只有一张电费单,上面只有总电量、峰平谷电量、基本电费、力调电费这几行字。老板觉得是生产班组浪费用电&a…

作者头像 李华
网站建设 2026/10/10 10:23:23

EmbeddingGemma 2 嵌入模型实战:本地语义搜索与性能优化指南

1. 这个模型到底解决了什么问题EmbeddingGemma 2 这个模型名字拆开看就很有意思。Embedding 是嵌入,Gemma 是模型系列名,2 是版本号。合起来就是一个专门做文本嵌入的第二代模型。文本嵌入这件事说白了就是把一段文字变成一串数字向量,让机器…

作者头像 李华
网站建设 2026/10/10 10:22:48

AI在金融风控中的应用原理与实践路径

我无法根据所提供的输入内容生成符合要求的博文。原因如下:输入中缺失关键字段:项目正文、关键词、摘要描述均为空(仅提供了一个标题和空的热搜词/热词区块),不符合【输入与处理流程】中规定的严格输入格式&#xff1b…

作者头像 李华
网站建设 2026/10/10 10:22:42

从零搭建AI持久化记忆系统:claude-mem架构设计与实操指南

1. 从零搭建一个持久化记忆系统:我为什么盯上了 claude-mem第一次看到claude-mem这个名字,我脑子里蹦出来的不是某个具体工具,而是一类长期困扰我的问题:大模型对话的"失忆症"。你肯定也遇到过——跟 AI 聊了半小时&…

作者头像 李华
网站建设 2026/10/10 10:22:39

变频器PWM载波频率如何影响电机噪声与损耗?现场调校指南

现场试机的时候,我经常会碰到这个情况:变频器一跑起来,电机就开始哇哇响,有时候是刺耳的“滋——”高频啸叫,有时候是低频的“嗡嗡”声。很多人第一反应是“电机坏了”或者“机械没装好”,实际上查到最后&a…

作者头像 李华
网站建设 2026/10/10 10:21:34

如何打造无可挑剔的代码:从命名到错误处理的工程实践

1. 从“impeccable”这个词说起:一个被低估的工程标准第一次看到“impeccable”作为项目标题,我愣了一下。这个词在英文里是“无可挑剔的、零瑕疵的”意思,日常对话中很少出现,更别说拿来当项目名了。但仔细一想,这恰恰…

作者头像 李华