ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

打造自己的命令行工具箱:Shell+Python自动化实践

打造自己的命令行工具箱:Shell+Python自动化实践

作为一个靠键盘吃饭的人,我每天做得最多的事情其实不是写新代码,而是反复执行那些早就做过一百遍的老活儿:批量重命名文件、把 CSV 转成 JSON、同步十几个 git 仓库、清理日志里的重复行、按目录归档素材。干这种活的挫败感在于,每一件都很简单,但加起来能无声无息吃掉一个上午。与其继续当人肉脚本机,我干脆从一年多前开始,把所有高频动作固化成了一套命令行工具箱,项目代号就叫 superpowers。这篇文章不聊宏大架构,就把它从 0 到 1 的完整思路、核心技术选型、六大常用模块,以及那些踩到脚疼的坑,原原本本拆给你们看。

1. 项目缘起:当重复劳动开始消磨耐心

1.1 那些让人想拍桌子的日常

最先让我意识到问题严重的,是一个周五的下午。当时我在准备一批素材文件,需要把 200 多个IMG_2024*_RAW.JPG改成带日期分组的目录结构,顺便把其中一半转成 PNG 缩略图。我用文件管理器手动拖了一个多小时,拖到后来手指发酸,脑子里只有一个念头:这种活儿,凭什么要我亲自干?

类似的场景实在太多了。打开终端一看历史记录,全是同一个命令翻来覆去地改参数:sed -i批量替换、find找文件、awk切列、git pull循环同步仓库。它们不是不会,而是每次都要重新想一遍参数,偶尔还会写错,而且完全没法保证所有机器上的行为一致。这种"会做但不稳定"的状态,其实比不会做更耗人。

1.2 从临时脚本到固定工具

一开始我也没想搞什么项目,就是在~/bin下面丢一堆.sh文件,用到哪个算哪个。但很快问题来了:脚本之间互相调用的时候,有人用source,有人直接拼接,变量命名乱成一锅粥;换一台电脑之后,依赖的jq、ffmpeg到底装没装,完全靠赌。

真正让我下定决心重构的,是有一次我帮同事跑一个文件处理脚本,结果因为环境里的python指向的是 Python 2,而脚本是按 Python 3 写的,直接崩了。那一刻我就明白,散装临时脚本永远不可能成为可靠的"超能力"。superpowers 这个项目要做的不只是把命令收集起来,而是给这些琐碎操作一套统一的入口、统一的参数风格、统一的输出格式,让它们在任何机器上表现一致。

1.3 为什么叫"superpowers"

取名 superpowers,想法很朴素:一个普通开发者和一个高效开发者之间,差的往往不是智商,而是一套顺手到不假思索的工具流。就像游戏里的技能按键,在特定场景下按一下,就能自动完成一整套复杂连招。我们希望把这些高频操作变成肌肉记忆,让使用者觉得自己好像突然有了某种超能力——一条命令完成过去要手动操作半小时的事情。

这个命名也提醒我一条设计原则:命令必须"足够短、足够直接",短到不需要查文档就敢敲。如果一条命令还需要翻 README 才能想起来怎么用,那它就还没达到 superpowers 的标准。

2. 核心技术选型:为什么选择 Shell 加 Python 的组合

2.1 各方案的取舍

在设计工具箱时,摆在我面前的无非四条路:纯 Shell、Python、Go、Node.js。我列过一张表,把每个方案的优劣势摊开来看:

方案上手成本依赖体积跨平台表现最适合的场景
纯 Shell低极小一般,Windows 上问题较多胶水调用、快速任务
Python 3中小好文本处理、文件批量操作、解析数据
Go中高单二进制极好需要高性能、需要单文件分发
Node.js中大好已有前端技术栈、生态偏 Web

对 superpowers 来说,项目核心是处理日常文件和文本,这些工作对性能并不敏感,更重要的是开发效率和移植门槛。Go 做出来的工具确实干净,但每次加一个模块都要编译、写样板代码,对个人工具库来说有点过度工程。Node 依赖体积大,而且我对它的 CLI 参数解析生态一直没有特别顺手。最终我选了"Shell 做胶水、Python 做重活"的混合方案。

2.2 以 Shell 做胶水、以 Python 做重活

这个分工背后的逻辑其实很简单:Shell 是一门天生的"命令编排语言",它最擅长的是启动进程、传递参数、检查返回值;但一旦遇到复杂的文本解析、JSON 操作、跨平台路径处理,Shell 的边界感就变得很模糊,写起来容易变成不可维护的"天书咒语"。

所以在 superpowers 里,绝大多数"超能力"入口都是一个 Shell 脚本,负责解析参数、检查环境、调用真正的处理逻辑;而核心的数据处理逻辑统一用 Python 3 写。比如sp file rename这个命令,Shell 层只负责告诉你--dry-run要不要输出预览、目标目录是否存在,真正处理 EXIF 信息、重名冲突的逻辑全部在 Python 模块里。这样两边各干各擅长的事,代码读起来也清爽很多。

2.3 项目结构设计

解决了语言选型,接着就是目录结构。我最早吃过"所有脚本堆在一个目录"的亏,后来把项目整理成了这样:

superpowers/ ├── bin/ # 所有子命令的入口脚本集中地 │ ├── sp │ ├── sp-file │ ├── sp-text │ ├── sp-git │ ├── sp-env │ ├── sp-net │ └── sp-doc ├── lib/ # 公用的 Shell 函数库 │ ├── log.sh # 统一日志输出、颜色、级别 │ ├── files.sh # 路径处理、目录初始化 │ └── os.sh # 操作系统检测 ├── modules/ # Python 核心处理模块 │ ├── file_ops.py │ ├── text_tools.py │ └── git_workflow.py ├── config/ # 全局配置 │ └── defaults.yaml └── docs/ └── recipes.md # 常用场景案例

统一入口是一个名为sp的符号链接,实际上指向bin/sp,由它再把任务分发给对应的子命令。这样设计带来的最大好处是:使用者只需要记住sp一个开头,然后按直觉补充第二个词,比如sp file、sp git、sp env,几乎不需要查帮助。命令数量多起来之后,这种命名一致性的价值会被放得很大。

3. superpowers 六大核心模块逐个拆解

3.1 sp-file:文件批处理与归档

文件操作是 superpowers 里最早上线、也最常用的一组命令。核心诉求只有两个:批量重命名和按规则归档。

批量重命名我做得比较谨慎,因为文件一旦改错,恢复成本很高。命令设计成了这样:

sp file rename --dir ./photos --pattern "IMG_*.JPG" --to "vacation_%02d.jpg" --dry-run

--dry-run是重命名、删除这类危险操作必需的模式,它只打印将会发生的动作,不真正落盘。输出类似:

[DRY RUN] ./photos/IMG_1.JPG -> ./photos/vacation_01.jpg [DRY RUN] ./photos/IMG_2.JPG -> ./photos/vacation_02.jpg [DRY RUN] ./photos/IMG_3.JPG -> ./photos/vacation_03.jpg

确认无误后,去掉--dry-run再跑一遍。所有会覆盖已有文件的操作,程序会默认拒绝执行,除非显式加--force。这个机制帮我避免了很多次"手滑式灾难"。

sp file archive则是把散落各处的文件按扩展名或日期归入指定目录结构。比如把下载目录里乱七八糟的文档、图片、压缩包各归各的类:

sp file archive --dir ~/Downloads --by-extension --target ~/Archive

背后的逻辑是shutil.move加同名冲突检测,Python 的标准库已经足够完成,不需要额外引第三方依赖。

3.2 sp-text:文本转换与清洗

如果说文件模块处理的是"外部世界"的杂乱,那文本模块处理的就是"内部世界"的混乱。日常写博客、写文档、处理数据导出的时候,我最常干的三件事是:格式转换、批量替换、编码修正。

其中使用率最高的是 JSON 与 CSV 互转:

sp text csv2json --input records.csv --output records.json --pretty

这个命令的内核是 Python 的csv.DictReader加json.dump。实际使用时我加了一个很关键的参数--encoding,因为从 Excel 导出的 CSV 很有可能是 GBK 编码,如果不处理,读取出来就是一排乱码。遇到这种情况,按下面的顺序先探测、再转换:

file records.csv sp text iconv --input records.csv --from gbk --to utf-8 --output records_utf8.csv

批量替换则参考了sed的用法,但做得更保守:默认要求用户最前面传一个"替换规则文件",每行一条旧文本|||新文本,避免在命令行里写大量需要转义的特殊字符。命令行太复杂,就容易出错,把规则放在文件里既好复查,也好复用。

3.3 sp-git:Git 工作流加速

Git 本身已经很强大了,superpowers 在它上面做的事情,只是把"多个仓库的重复操作"压缩成一条命令。

我最常碰到的场景是同时维护好几个独立项目,每天上班第一件事就是把它们全部更新到最新:

sp git sync --dir ./workspace --remote origin --branch main --depth 50

这条命令会遍历指定目录下的所有 git 仓库,跳过还没有初始化的目录,依次执行fetch、ff-only合并、报告脏工作区。它有个贴心的小设计:哪个仓库没有干净工作区,就先跳过并明确标注,绝不擅自stash或checkout -- .,避免把你没提交的改动丢掉。

另一条高频命令是提交信息规范化:

sp git commit-template --type feat --scope user-api --summary "增加登录重试机制"

它会拼接出一个符合约定式提交规范的 message,再让你确认后提交。一开始我觉得多此一举,但用了两个月之后,回看历史提交记录,那种一致的格式感真的会让git log --oneline的可读性提升一个档次。

3.4 sp-env:环境初始化与配置管理

换新电脑、重装系统、给新加入项目的同事配环境,这些事看起来都是"小概率但每次都要折腾半小时"的活。sp-env 要解决的就是让环境初始化尽可能无脑。

sp env setup --profile dev

这个命令会读取config/defaults.yaml,根据当前操作系统自动选择包管理器安装常用工具。比如 Linux 上会优先判断是apt还是dnf,macOS 上则调用brew。安装之前会先把将要执行的安装列表完整打印出来,让使用者确认一遍,避免一顿操作猛如虎之后才发现装错了东西。

这里我踩过一个大坑:不要试图在setup里把所有工具都装一遍。工具链膨胀会让初始化时间从五分钟变成四十分钟,而且很多工具一辈子都用不上。现在我的做法是拆成profile-core、profile-python、profile-frontend三档,按需选择,轻量单独跑其中一档只需要两分钟。

3.5 sp-net:本地网络状态排查

这个模块不是为了处理复杂网络架构,而是解决一个天天都会遇到的烦心事:为什么连不上、哪个端口被占了、内网服务到底起没起。

sp net check --host localhost --port 8080 sp net ports --match 8080

第一个命令用来确认某个服务端口是否正在监听,第二个命令用来定位是哪个进程占了端口。排查端口冲突时,它会同时给出 PID 和进程名,省去lsof加ps来回组合的麻烦。

判断服务健康状态也有一个精简命令,可以做简单的 HTTP 状态码检查:

sp net http-status --url http://127.0.0.1:8080/health --expect 200 --retry 3 --interval 2

--expect 200会在连续三次访问中只要有一次返回 200 就判定成功,适合用来等待本地服务就绪。写自动化脚本时,这个等待逻辑反复用到,能省掉不少自己写轮询循环的精力。

3.6 sp-doc:文档输出与格式规范

最后一个模块是为了我写技术文章和接口文档服务的。写文章最痛苦的不是动笔,而是格式乱七八糟:全角半角标点混用、段落之间多个空行、标题层级跳来跳去。

sp doc tidy --input draft.md --output draft_tidy.md

这个命令会做以下清理:统一中文全角标点、把连续三个以上的空行压成一个、修正常见的中英文之间缺空格问题、检查标题层级是否从一级连续递增。它不改变你的语义,只处理排版层面的强迫症需求,跑完顺手看一眼 diff 就能直接提交。

后来我还给这个模块加了一个接口文档生成小助手,能从带注释的 Python 函数定义里抽取参数说明,生成 Markdown 表格。诚实说,它没有商用 API 文档工具那么智能,但胜在零配置、离线可用,对内部工具足够友好。

4. 真实使用场景:一次上线流程里的效率对比

4.1 场景设定

有一次我负责一个前后端联调功能的上线,整个流程可以拆成下面几个阶段:更新多个仓库代码、整理配置文件、批量替换测试环境的域名、检查依赖服务状态、最后写部署记录的摘要。放在以前,这五个阶段每一阶段都需要打开好几个终端窗口,翻历史命令,手动校对。

4.2 使用 superpowers 之前的操作流

老流程大概是这样的:

# 逐个仓库 cd 进去 git pull,有的仓库还要处理本地冲突 cd app-frontend && git pull cd ../app-backend && git pull cd ../shared-lib && git pull # 拷贝配置模板 cp api-example.yml api.yml # 手工把测试域名替换成生产域名,打开编辑器逐行改…… # 用浏览器/curl 反复确认服务起来了没 curl -s http://localhost:8080/health ps aux | grep node

这些步骤单独看都不难,真正折磨人的是上下文切换。每次从一个仓库切到另一个仓库,都要重新回忆"这个仓库用的是 main 还是 master"、"它的依赖装好了没有"。一套下来,半小时起步,而且中途特别容易被打断,一打断就得从头理思路。

4.3 使用后的操作流

在 superpowers 的帮助下,同一件事变成了:

# 1. 所有仓库同步 sp git sync --dir ./work --remote origin --branch main # 2. 根据工作目录生成配置文件 sp env fetch-config --service api-gateway --from-template --replace-env prod # 3. 批量替换配置里的域名占位符 sp text replace --input api.yml --rules ./rules_prod.txt --dry-run # 4. 确认依赖服务端口都在线 sp net check --host 127.0.0.1 --port 8080 sp net check --host 127.0.0.1 --port 3306 # 5. 生成提交记录摘要 sp git log-today --dir ./work --format compact

每一步都是单条命令,每一步的输出都很克制,不会刷屏。中间被同事打断十分钟,回来扫一眼终端记录就能知道进行到哪一步了,完全不用重新理一遍上下文。

4.4 时间对比与真实感受

同样的流程,我专门掐过一次表:老方法在完全顺畅的情况下是 28 分钟;用 superpowers 首轮跑通是 7 分钟,而且其中大部分时间是在等sp git sync的 fetch 网络时间。省的 20 多分钟,主要省在"不打断"和"不回忆"上。

但我也要诚实说,这 7 分钟里,有 3 分钟是在--dry-run模式下检查替换结果和确认危险操作。这个时间我不会省,因为它换的是安全感。工具越强,越要在危险动作前保留一道人工确认的闸门。

5. 踩坑实录:路径、编码、跨平台,三个难缠的问题

5.1 路径中有空格:所有参数都要加引号

superpowers 上线后接到的第一个"工作事故"报告,是有个同事执行sp file rename时,所有路径带空格的目录全报错了。我排查的时候发现,Shell 层解析参数时直接把整个路径拆成了两段,传给 Python 之后就出现了"找不到文件"。

这个坑的本质原因其实很经典:在 Shell 脚本里,变量展开后必须用双引号包起来。比如:

# 错误示范 python3 module.py --path $dir # 正确示范 python3 module.py --path "$dir"

一句话的事,但一旦在循环里写,就很容易犯。我后来给 lib 层加了一个参数校验函数,任何包含空格或特殊字符的路径都会被单独标记出来,并且强制转换为数组参数传递,从根上杜绝了这个坑再次出现。

5.2 编码混乱:UTF-8 的"假统一"

处理文本文件时最让人头皮发麻的,不是文件不是 UTF-8,而是"它看起来是 UTF-8,其实不是"。很多从旧 Windows 机器上拷过来的文本,实际编码是 GBK,但文件扩展名照旧是.txt,编辑器也能正常打开——因为编辑器默默做了自动探测,而 superpowers 里的 Python 可不会自动猜。

排查这类问题,我推荐用这条链路:

file my_note.txt # 输出: my_note.txt: ISO-8859 text, with CRLF line terminators

看到ISO-8859或non-UTF-8,直接转码:

sp text iconv --input my_note.txt --from gbk --to utf-8 --output my_note_utf8.txt

我总结出的经验是:所有涉及文本读取的模块,默认都先做一次轻量探测,发现非法 UTF-8 序列就明确报错,并提示用户指定编码,而不是自作聪明地猜。猜错了还不如报错。

5.3 跨平台兼容:不要相信 rm -rf 在 Windows 上的脾气

superpowers 最初只在 macOS 和 Linux 上跑,直到有一次我在 Windows 的 Git Bash 环境里演示,才发现问题远不止路径分隔符那么简单。

第一层问题就是/与\。Python 里用pathlib处理路径相对安全,但 Shell 层的字符串拼接会直接把 Windows 路径里的\当成转义符吃掉。第二层问题是命令差异:Linux 上的rm -rf、cp -r、grep -r在 Git Bash 里多数能用,但行为偶尔不一致,比如参数顺序、通配符展开规则。

我的解决思路是"尽量少在 Shell 层处理真实文件",只把路径原样传给 Python,由pathlib统一处理。同时给 os.sh 加了一层能力抽象,比如safe_rm在 Windows 上优先调用 Python 的shutil.rmtree,而不是直接rm -rf。这样同一套命令在三个系统上的表现基本一致,剩下的小差异再用文档明确标注。

6. 如何搭建属于你自己的超能力工具箱

6.1 先盘点你的高频动作

打造自己的 superpowers,第一步不是写代码,而是盘点。我最推荐的方法是把过去两三个月的终端历史从头翻一遍,统计出现频率最高的命令和参数组合。

以 zsh 为例:

history 1 | awk '{print $2}' | sort | uniq -c | sort -rn | head -20

看到那些反复出现七八次的命令,就是你最该固化的"超能力"候选。比如如果你的历史记录里满是python manage.py migrate、docker compose up,那就围绕这些命令包一层更加语义化的封装,减少每次敲全命令的负担。

6.2 一个工具只需要做好一件事

在设计 superpowers 的过程中,我最深的体会是:模块可以多,但每个命令的职责必须单一。sp file rename就只管重命名,不要顺手做压缩;sp git sync就只管同步,不要顺手清理分支。职责一旦交叉,命令的参数数量就会爆炸,最后变成一个什么都能干但什么都难记的"瑞士军刀怪兽"。

如果你发现自己正在给一个命令加第五个可选参数,最合理的操作不是继续加默认值,而是拆出一个新命令。

6.3 文档即记忆:写 README,而不是靠大脑

再顺手的命令,一个月不碰也会生疏。我在项目里强制执行一条规则:每个命令必须有对应的 README 段落,至少包含"一句话说明"、"常用示例"、"危险参数警告"三部分。

模板我固定成这样:

### sp file rename 一句话说明:按规则批量重命名文件。 常用示例: sp file rename --dir ./photos --pattern "*.JPG" --to "vacation_%02d.jpg" --dry-run 危险参数警告: --force 会覆盖同名文件,使用前必须确认 dry-run 结果。

有了文档,你就不再依赖记忆。换个电脑、隔三个月回来,翻一眼文档就能立刻捡起来。这一点,远比任何复杂的自注释代码都重要。

6.4 从 v0 到 v1 的迭代节奏

最后聊聊迭代节奏。superpowers 不是一天建成的,我的第一个版本只有两个命令、几百行代码,而且处处是硬编码的绝对路径。但现在回头看,这种粗糙的 v0 反而值钱,因为它让我立刻进入真实使用场景,而不是花一个月设计一个"完美但没人用"的框架。

我的建议是:先挑一个你最痛、最频繁的操作,做出一个能用的命令,立刻放到日常里去用。用两周之后,你自然会知道要加什么参数、要改什么输出、要防什么错误。然后带着这些真实反馈,再去做第二个命令。这样滚雪球式地迭代,会比一次性设计十个模块靠谱得多。

我个人在实际操作中还有个习惯:每次觉得"某个重复动作好烦"的时候,不硬扛,先记到待办清单里,等凑够三五个类似痛点再统一设计。这样既不会让自己陷入琐碎工具的泥潭,又能保证每一个新模块都来自真实需求。superpowers 这个项目到现在已经陪我走了一年多,它最大的价值不是省了多少时间,而是让我在动手做事之前,心里多了一份"这件事我有顺手的招"的笃定。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进