ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

把重复操作变成命令:CLI-Anything方法论与实用脚本库

把重复操作变成命令:CLI-Anything方法论与实用脚本库 周末在家整理照片一个朋友凑过来看我在干嘛屏幕上一个黑乎乎的终端窗口光标一闪一闪的。他问“你这又是干嘛明明双击就能打开文件夹。”我笑了笑把一条命令敲下去几百张照片按日期自动归档成了目录树。没过多久我又用另一个命令批量压缩了上周拍的一堆视频素材顺手把一份PDF转成了带书签的文字稿。他看愣了“你这是把什么都变成命令行了吗”对这就是我做的一个项目我管它叫CLI-Anything。它不是一个庞大的软件也不是什么框架而是一套“凡是用图形界面重复做了两遍以上的事我就会尝试把它写成一条命令”的方法论。核心思想很简单把零零散散的重复操作沉淀成可以复用、可以组合、可以自动触发的小命令。这个仓库里装的是我日常用到的脚本、函数、别名和工具链覆盖文件整理、批量处理、数据抓取、任务通知甚至开会前的资料准备。就像一个随身工具箱走到任何一台新电脑上都能在几分钟内恢复我熟悉的工作姿势。不管你是刚接触终端的新手还是已经写了几年脚本的老手这篇博客都适合你。新手能从这里看到“命令行到底能干什么”的落地场景老手也能对照检查自己有没有踩过类似的坑。我会把设计逻辑、脚本结构、失败的尝试和最终保留下来的经验都摊开讲。1. 为什么要把“Everything”都塞进一条命令里1.1 GUI适合探索CLI适合重复很多人觉得自己用不上命令行因为图形界面够用了。这个判断在“偶尔做一次”的场景里成立但一旦开始重复图形界面的效率就会断崖式下跌。我给你打个比方图形界面像超市货物摆得整整齐齐你每次想买一瓶酱油都要从入口走到调味区经过几十个货架偶尔还会被促销堆头挡住。而命令行像你家里固定位置的储物架第一次收拾好之后之后每次闭着眼就能摸到。举一个最普通的例子批量重命名文件。在文件管理器里你要一个一个右键、重命名、输入新名字遇到几十个文件的时候那感觉就像在流水线上拧螺丝。换成命令行一条循环配合参数展开瞬间搞定。更重要的是图形界面里的操作是不可复述的你很难告诉别人“我刚刚是怎么点出来的”而命令行里的一条命令本身就是操作过程的完整记录也是可以随时回放的操作说明书。CLI-Anything 的核心动机就在这里把重复劳动变成可复用的命令让每次操作的成本趋近于零。图形界面用来“发现”命令行用来“重复”。这是我试过很多轮之后最深刻的体会。1.2 我选择的“底座”POSIX shell 高频小工具很多人一听说要把事情命令行化第一反应是“那我需要会编程”。其实这里面有个误区。我做 CLI-Anything 的过程里主力语言并不是 Python也不是 Go而是POSIX shellBash/Zsh 一批 Unix 小工具。为什么选这条路线因为 shell 的胶水能力太强了。它有管道pipe可以把一个程序的输出直接喂给另一个程序当作输入这是图形界面和大多数高级语言里都不太容易做到的组合方式。比如我需要把一批日志里含“error”的行按时间排序再统计出现次数写成一个 Python 脚本要十几行用 shell 可能就是rg error app.log | sort | uniq -c | sort -rn四段管道一气呵成。哪怕你完全不懂编程也能大致猜到每一步在干什么先找包含 error 的行排序统计次数再按次数倒序排。这种“可读的执行链”就是命令行最迷人的地方。除此之外我几乎每个脚本里都会用到几个高频工具rg快速的文本搜索、fd好用的文件查找、fzf交互式模糊选择、jqJSON 数据解析、parallel并行执行。这些都是命令行生态里经过千锤百炼的零件比自己写函数靠谱得多。我不需要记住每个工具的全部特性只需要知道它们能做什么需要的时候再查帮助文档然后像乐高积木一样拼起来。1.3 什么不该放进CLI-Anything选择命令行不等于拒绝图形界面。我做了这么多命令之后反而更清楚它的边界。交互型、探索型的操作更适合图形界面。比如看一张照片的构图、对比两份设计稿的视觉效果、编辑一段音轨的波形这种事情用命令行做就是给自己找罪受。CLI 的价值在于“确定性的重复”而 GUI 的价值在于“没有固定路径的探索”。我在 CLI-Anything 的 README 里专门写了一节“什么不该做”提醒自己如果一个操作经常需要临时调整方向就别硬塞进命令里如果一个操作一个月都用不到一次也别急着写脚本。另外凡是需要极高安全性的敏感操作我也不建议直接脚本化。比如删库、批量删除文件、覆盖重要数据这类操作应该保留一层人为确认的屏障。你可以把命令写出来但最好加一个确认参数或者至少加个--dry-run让你先看一眼结果。2. 第一类落点文件与信息检索的日常命令化2.1 批量重命名先打印再执行我最早尝到“把一切命令行化”的甜头就是从批量重命名开始。那会儿刚拍完一趟旅行相机导出来一千多张照片文件名全是IMG_4821.jpg这种毫无信息的编号。我想按“日期_拍摄顺序”的方式整理折腾了一晚上最后还是用一行循环解决的for f in IMG_*.jpg; do date_part$(stat -f %Sm -t %Y%m%d $f) mv $f ${date_part}_${f#IMG_} done解释一下关键点stat拿文件的修改时间格式化成YYYYMMDD然后把新文件名拼接成20250518_4821.jpg的形式。这里面有个很重要的小心思——先把所有新名字打印出来看看再真正执行 mv 操作。我习惯在改动文件之前加一个“试运行”模式比如在命令前面加echo代替mv先观察一遍有没有明显的错乱确认之后再去掉 echo 执行。这个习惯帮我避过很多坑。有一次批量重命名因为文件名里包含空格脚本直接把一个文件裁成了两半如果不是先打印了一遍文件就彻底乱了。遇到这种情况正确的做法是用引号包住变量mv命令的两个参数都要用双引号括起来防止空格和特殊字符被当作分隔符。2.2 检索与跳转rg、fd、fzf 的三层配合文件多了之后“找到某个文件”本身就成了高频操作。图形界面里的搜索框经常慢得令人发指而且搜完你还得一层层点开文件夹。我把这套检索流程做成了三层配合fd负责按名字和目录快速找文件支持正则表达式默认还会忽略.git这类隐藏目录rg负责按文件内容搜索比如在几百个文档里找包含“季度报告”关键词的文件fzf负责提供一个交互式的选择界面把前两者输出的候选列表变成你可按键选择的菜单。实际用起来长这样fz() { local file file$(fd --type f | fzf --preview head -100 {}) if [ -n $file ]; then $EDITOR $file fi }这个函数叫fz运行之后屏幕上会列出当前目录树里的所有文件模糊输入几个字符就能缩小范围选中之后自动用编辑器打开。整个过程里我动鼠标的次数为零。“找到并打开一个文件”这个动作被我压缩成了一次按键选择。内容搜索同理。比如我不记得某个配置文件在哪只记得里面有个叫做timeout的设置那么一条rg -l timeout ~/.config就能列出所有涉及这个关键词的文件。这条命令本身就变成了我对整个配置文件体系的“全文索引”。2.3 一次性归档脚本的设计整理照片、归档下载目录、清理临时文件这些任务都有一个共同特点频率不高但每次来了就是一堆重复动作。我给这类任务专门写了一个archive脚本核心逻辑是“按扩展名和修改日期分流”。比如归档下载目录会先扫描每个文件判断它是文档、图片、压缩包还是安装程序然后移动进对应的类型文件夹同时对超过 30 天没碰过的文件单独列到“待清理”清单里而不是直接删除。这个脚本的好处是它把判断规则固化了下次做同样的事情不需要重新回忆“我上次是怎么分的”一条命令就搞定。脚本里我比较得意的一个设计是先完成分析和预览把操作分成两步。第一步只输出“这个文件将被移动到哪”第二步才是真正的mv。用户可以在第二步前中止也可以传一个-y参数跳过确认。这种“默认安全显式放行”的模式成了我后来所有脚本的标准姿势。3. 面向杂活的自动化脚本OCR、PDF 与格式转换3.1 一个可复用的OCR小脚本我的日常工作中有一类特别烦人但又特别常见的任务把纸质材料的扫描件转成可检索的文字。截图也好、扫描 PDF 也好落在我手里都像是“图片里的文字”。于是我给 CLI-Anything 加了一个 OCR 工具链整套流程用到了两个开源工具pdftoppm能把 PDF 的每一页转成图片tesseract负责识别图片里的文字。核心脚本大概长这样#!/usr/bin/env bash set -euo pipefail PDF$1 OUT${2:-output} tmpdir$(mktemp -d) trap rm -rf $tmpdir EXIT pdftoppm -r 300 $PDF $tmpdir/page for img in $tmpdir/page-*.ppm; do tesseract $img ${img%.*} -l chi_simeng 2/dev/null done cat $tmpdir/page-*.txt $OUT.txt这里有几个值得细说的点。第一-r 300指的是渲染分辨率 300 DPI我实测过大部分 180~300 都能较好识别太低了文字边缘糊成一片太高了文件大、速度慢300 是性价比比较高的档位第二tesseract指定了chi_simeng让中英文混排的文档也能同时识别这个参数是后装的识别效果比默认英文模型好很多第三脚本用mktemp -d建了一个临时目录工作完自动清理不会在桌面上留下一堆中间图片。这套脚本解决了我一个很大的痛点以前手动操作是“打开 PDF → 导图片 → 逐张 OCR → 拼文本”现在只需要ocr_pdf 合同扫描件.pdf 合同文本几分钟后合同文本.txt就躺在旁边了。我一直说好的工具是让你忘了工具本身的存在。这条命令做到了。3.2 批量压缩与转换的流水线处理图片和视频的批量化也是 CLI 的高光场景。我在项目里写过一个压缩图片的脚本用find找到目标目录下的所有 JPG再用parallel并行调用 ImageMagick 的convert做压缩最后统一把文件尺寸控制到适合网页阅读的大小find $1 -type f \( -iname *.jpg -o -iname *.png \) -print0 \ | parallel -0 convert {} -resize 1920x1080 -quality 80 {.}.webp这里解释一下几个参数-resize 1920x1080里的尖括号表示“只对大图生效小图不放大”-quality 80是输出 WebP 的压缩质量{.}在parallel里代表去掉扩展名的原路径所以photo.jpg会输出成photo.webp。为什么要转 WebP因为同样的视觉效果下WebP 的体积通常只有 JPEG 的一半。实测一组 20MB 的照片压缩之后总共只剩 6MB画质肉眼几乎无差别。视频转码也是类似。我的标准命令是ffmpeg -i 输入.mp4 -c:v libx264 -crf 23 -preset medium -c:a aac。-crf 23是压制质量参数数字越大画质越低文件越小23 属于“看得过去”的平衡点-preset medium是编码速度和压缩率的折中。遇到只是想临时压小发微信的场景我会换成-crf 30 -preset fast速度起飞文件极小。3.3 在脚本里管住错误和中间文件写这些自动化脚本的时候踩过最多坑的反而不是命令本身而是“出错之后怎么办”。一个脚本跑一半因为某个文件打不开中断了前面生成了一堆临时文件后面啥也没干成这是最让人抓狂的。所以我在 CLI-Anything 的绝大多数脚本开头都加了三行set -euo pipefail意思是-e遇到第一个错误就退出-u使用未定义变量直接报错-o pipefail让管道中任何一环失败都算失败。这样脚本就不会在错误状态下硬着头皮继续跑。此外配合trap做清理保证脚本无论是正常结束还是中途崩溃临时目录都会被收拾干净。我会在后面的第 6 章专门讲这个细节里的坑。4. 用一条命令触发“工作流”任务、通知与轻量协同4.1 长任务结束后的手机通知我平时会跑一些耗时的任务比如照片批量压缩、视频转码、数据爬取。这些任务跑起来动辄十几分钟人又不能一直守在电脑前刷进度。CLI-Anything 里我最喜欢的一个小脚本就是“任务跑完通知我”。它的本质很简单用curl向手机上的通知服务发送一个推送请求。比如使用某个支持 Webhook 的推送 API只需要在任务结束后发一个 HTTP 请求notify() { local msg${1:-done} curl -s https://api.notify.service/v1/send \ --data-urlencode message任务完成$msg \ -H Authorization: Bearer $TOKEN /dev/null }然后我所有的长任务都会写成这种形式compress_video big.mp4 notify 压缩完成实际使用的过程里我觉得最舒服的不是推送本身而是它反向改变了我的工作习惯。以前跑长任务人总忍不住时不时切回来看一眼现在我可以放心去拖地、泡茶手机响了就知道好了生活里少了很多无意识的刷新动作。4.2 开会准备、待办与日程的“一个命令”除了电子杂活我把一部分生活事务也试着变成了命令其中“开会前准备”是我实践下来收益最明显的场景。每个周一早上我都要给团队整理项目进度以前要手动创建文档、贴模板、开录音软件、打开相关资料文件夹。我把这个流程做成了一条初始化命令meeting_prep() { local today today$(date %Y-%m-%d) local filemeeting/meeting-${today}.md [[ -d meeting ]] || mkdir -p meeting if [[ ! -f $file ]]; then cat $file EOF # 会议记录 ${today} ## 待确认事项 ## 行动项 EOF fi $EDITOR $file open meeting/ }运行之后它会自动创建当天的会议记录模板打开编辑器再把会议目录也弹出来。如果那个会议是线上进行还可以追加一句record 开始录音。这样一套组合下来原本 5 分钟的准备压缩成了“敲一条命令 编辑内容”。待办管理也是类似思路。我没有用什么花哨的待办软件而是维持了一个纯文本的todo.txt配合两个别名alias tcat ~/todo.txt alias taecho \$(date %m-%d) \$* ~/todo.txtt查看所有事项ta 给路由器换位置就是追加一条待办。纯文本的好处是永远不会因为软件倒闭而丢失也随时可以用rg搜索、用sort排列任何平台都能读取。可能有人觉得这太简陋了但越简单的方案越不容易丢弃我已经这样记了一年多。4.3 从命令到流程在组合中发现“乐高效应”很多刚接触 CLI 的人会误以为这些命令是孤立的。实际上管道的存在让一切单个命令都有了拼接的可能。我之前处理一周的备忘录时会用一个简单流程rg 待办|TODO|todo ~/notes -i \ | sed s/^/今天要处理: / \ | head -20先把所有笔记里待办相关的行找出来用sed加个前缀让列表更好读最后取前 20 条。这一段流程里的每个零件单独看都不稀奇但组合起来整个操作变成了一个“一次输入、一步到位”的查询系统。这种“乐高效应”是 CLI-Anything 最核心的方法论先积累零件再组合流程。不要一开始就试图设计一个能解决所有问题的大脚本而是先写一堆只做一件小事的小命令等到某天发现“我需要连续做三件事”再去把三件事串起来。组合出来的新命令又可以沉淀为新零件循环往复。5. 把散落脚本整理成可迁移的 CLI 工具箱5.1 目录结构与命名规范脚本写多了以后最大的敌人不是不会写而是找不到自己的脚本。早期我是一个文件里塞十几个函数时间一长连自己都忘了里面有什么。后来我按照功能域做了拆分目录结构大概是这样的~/.local/bin/ # 所有可执行脚本放这里PATH 直接包含 ├── todo.sh # 待办、日程类 ├── media.sh # 图片、视频、PDF 处理 ├── note.sh # 笔记、检索、OCR ├── notify.sh # 通知推送 ├── fmt.sh # 文件名与文本格式化 └── net.sh # 网络请求、抓取命名上我坚持全部小写、不用空格这样在任何地方敲命令都不需要转义。每个脚本头部都会写清楚依赖和示例像这样#!/usr/bin/env bash # 用途: 批量压缩图片为 webp # 用法: compress_images 目录 # 依赖: parallel, convert (imagemagick)这个注释习惯帮了我大忙。有一次隔了三个月想改一个脚本打开文件一眼就看到用法和依赖完全不用重新读一遍代码才想起来这东西是干嘛的。5.2 用Git管理自己的命令仓库脚本整理好之后我直接把整个~/.local/bin放进了 Git 仓库同步到自己的私有远程仓库。这样带来的好处非常实际换新电脑的时候git clone下来再把目录加进PATH我熟悉的命令环境几乎原地复活。我还加了一个轻量级的安装脚本做的事情只有三件检测依赖、把bin目录链接到当前用户目录下、往.bashrc/.zshrc里追加PATH和别名。整个过程十分钟内搞定。有了这个“一键安装”我敢放心在自己电脑上实验各种新玩法因为就算改崩了也能回到上一个版本。版本管理不只是给自己用。每次我改进了一个脚本顺手git commit一下积累下来其实是一份很个人化的“命令进化史”。回头翻看能清晰看到自己在处理同一类事情上的思路演变。5.3 让同事也能用起来文档和演示我以前有个错误的观念好东西自己用就行分享出去要解释半天麻烦。后来负责一个小项目需要带着几个同事一起做重复性很高的数据整理工作我才意识到一个人会写命令不如让团队里的所有人都能用上命令。为了分享我把脚本仓库做了两件事。第一件是为常见场景写了很短的使用说明每条只写三行问题描述、一句命令示例、输出效果。第二件是做了一个 demo 脚本用一组模拟数据把所有命令依次跑一遍让大家能直观看到输入输出长什么样。这比写几百行文档有效得多因为人在不熟悉命令行的时候第一反应是“这玩意儿安不安全”演示恰好能回答这个问题。当然分享之前要收拾好脚本。我把所有脚本的路径写死部分都改成了相对路径或环境变量把个人信息和 token 挪到单独的配置文件中避免别人拿到脚本之后还要逐个改路径。分享自己写的工具本质上是一次面向陌生人的“代码审查”虽然累一点但能逼你把代码整理得更像样。6. 命令行实践里最容易翻车的几个细节6.1 空格、中文与换行命令行的老坑做 CLI-Anything 的过程中绝大部分脚本 bug 不是逻辑错了而是被“不体面的文件名”坑的。你永远不知道用户目录里会出现什么样的文件带空格的、带中文的、带表情符号的甚至长得像命令参数的比如文件名以-开头。有一个经典误操作用for f in $(find . -name *.png)去遍历文件。如果文件名里有空格这个命令会把一个文件拆成两个元素轻则处理错误重则把不存在的文件路径传给下一个命令。正确做法是用find -print0配合xargs -0也就是用空字符而不是换行分隔文件名find . -type f -print0 | xargs -0 -I {} sh -c echo 处理: {}另一种更省心的办法是尽量用fd带--format输出自己需要的字段它会负责处理好引号和特殊字符。我已经把“永远用引号包住变量”写进了自己的肌肉记忆里效果立竿见影。6.2 set -euo pipefail 与管道“假失败”前面提到我在所有脚本开头都会写set -euo pipefail用下来确实减少了大量“带病运行”的情况但它也会引入一个看起来莫名其妙的失败场景。比如下面这条命令在set -e的脚本里可能跑着跑着就退出了rg error log/*.txt | wc -l原因是如果rg一个日志文件都没搜到它的退出码不为 0而pipefail会让管道整体退出码也变成非 0。可你的本意只是“统计错误行数没有就算了”。这种“假失败”在带set -e的脚本里很常见。解决方式有两种。一是临时允许失败set e count$(rg error log/*.txt | wc -l) set -e二是用rg自带的--quiet或|| true把那种“没有匹配不是错误”的语义显式表达出来。我在自己的仓库里经过反复取舍最终的默认是脚本一律带set -euo pipefail但凡是搜索、删除、统计这类“故意容许空结果”的操作单独处理。默认严格显式放开这样既不会被静默错误坑也不会被假失败误伤。6.3 跨平台兼容性和惰性使用的取舍最后想聊一个偏理念的坑跨平台兼容性。我在 macOS 上写完脚本换到 Linux 服务器上跑经常遇到同一命令行为不一致。比如sed -i在 macOS 上要求指定备份后缀stat的日期格式参数也完全不同md5和md5sum更是两个世界的产物。最开始我的目标是写一套“到处能跑”的脚本于是脚本里塞满了if [[ $(uname) Darwin ]]的兼容分支写着写着发现脚本比业务逻辑还长。后来我做了个更务实的决定只在脚本头部声明它运行在哪个平台最常见的是 macOS 本地和 Linux 服务器两套分别对应两个入口。跨平台共享的只有那些确定性的功能比如rg、fd、jq这些本身就是跨平台一致的现代工具。与其勉强统一所有细节不如把兼容性的边界控制在自己能应得的范围内。命令行生态有个非常有意思的现象专一的小工具会越来越像标准而试图包揽一切的复杂工具往往被弃用。这句话既适用于工具也适用于我整理 CLI-Anything 的方式不为迎合理论上的完美兼容去写过度复杂的代码只保留“明天我还会用”的部分。最后分享一个我在实际使用中才慢慢想明白的体会命令行不是“一劳永逸”的东西它更像一个每天需要维护一点点的花园。今天加一个别名明天改一个参数后天把一个碎片脚本彻底删掉——这个过程本身就是和自己的工作习惯反复对话。也许你不必把“每件事”都命令行化但在下一次重复操作到第六遍的时候打开终端试一次你会忽然发现自己已经离不开这种感觉了。
返回列表