ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

打造个人命令行工具箱:从批量重命名到OCR与自动化工作流

打造个人命令行工具箱:从批量重命名到OCR与自动化工作流 前阵子整理工作环境的时候我突然意识到一件事自己日常处理的任务里有很大一部分其实都能用命令行解决甚至用命令行解决才是最高效的。文件批量重命名、图片压缩、二维码生成、日志检索、Git 仓库初始化、定时提醒……这些事一多就催生出了一个念头干脆把能 CLI 化的事情全部 CLI 化做一个属于自己的命令工具箱。这个项目我起名叫做 CLI-Anything顾名思义就是万物皆可命令行。CLI-Anything 不是一个开源框架也不是某个现成的工具。它是我给自己搭的一套个人命令行工作流集合核心思路是用一套统一的命令前缀、模块化管理、合理的补全和帮助系统把散落在各个脚本、alias、第三方命令行工具里的能力整合起来让重复性操作变成一条短命令。如果你和我一样日常大量工作在终端里完成又经常觉得这个操作为什么不能一键搞定那这篇总结应该能给你不少启发。接下来我按设计和落地两个维度把整套方案拆开讲清楚。1. 为什么要把所有事都塞进命令行1.1 这个项目解决的真实痛点先说个场景。以前我要把一批照片按拍摄日期重命名第一反应是打开某个图形化批量工具选中文件、设置规则、预览、执行一套流程下来少说两分钟。如果照片在远程服务器上还要先下载到本地处理完再传回去中间那几步简直折磨。后来我改成一条命令cb rename --pattern IMG_*.JPG --replace 2024-{date}一秒执行完还不会覆盖原文件想反悔就靠演练模式先看结果。这类痛点其实不止存在于多媒体文件处理。日常开发里我需要频繁切目录、找文件、开仓库、查系统状态生活里需要记账、定闹钟提醒、转换文档格式。这些都是零散需求GUI 工具虽然能解决单点问题但没法串联、没法远程、没法重复使用。CLI-Anything 要解决的正是这么一个问题把一个个孤立的操作收敛成一套统一的、可编程的、可组合的命令体系。另一个隐性痛点是环境切换。我经常在 macOS 和 Linux 服务器之间来回切图形界面工具在两边的可用性差异很大但命令行工具反而基本通用。把操作收敛到 CLI 之后,换机器的成本会明显下降因为命令名、参数、行为都是我自定义的走到哪带到哪。1.2 设计核心原则CLI-Anything 在思考初期就定了四条原则后续所有模块都是围绕它们展开的。第一短命令优先。命令名要短、要容易记住。我给所有命令统一加了一个cb前缀可以理解成 cli-box 的意思后面跟动词和对象。比如cb rename、cb qr、cb todo。前缀的作用是避免和系统现有命令冲突同时给你的命令们一个独立的命名空间这在后面会详细展开。第二模块化。每个功能点是独立脚本互不依赖。我自己是用 Python 写的核心脚本加上少量 shell 封装。你也可以用纯 Bash、Rust、Go什么顺手用什么关键是模块之间不产生纠缠。第三可观测。命令在执行过程中要清清楚楚告诉你我在做什么、结果如何。所以几乎所有模块都统一输出日志带颜色分级支持--dry-run演练模式。这点看着简单实际对日常使用体验的提升非常大。第四可迁移。所有命令依赖统一收敛到一个配置目录里跨机器部署时只需要备份一个目录而不是七零八落散在各处的脚本和 alias。这四条原则本质上回答的都是同一个问题一个命令系统怎么才能让人真正愿意天天用。答案就是快、稳、透、可搬。后续所有开发都围绕这四点来凡是违反原则的功能我都会砍掉或重写。2. 目录结构与核心函数库设计2.1 命令从哪来目录与 PATH当初在搭建骨架的时候我踩过一个特别蠢的坑把脚本堆在一个地方alias 写在另一个地方配置文件放在第三个地方。结果没出一周就乱了想加个新命令得先回忆上次那个脚本到底放哪了。所以现在 CLI-Anything 的目录结构从一开始就固定得很明确~/.cliany/ ├── bin/ # 所有入口脚本放进 PATH ├── lib/ # Python/Shell 公共函数库 ├── config/ # 全局配置、环境变量 ├── logs/ # 运行日志统一按天切分 ├── data/ # 各模块产生的数据文件比如 todo 列表、账本 └── completions/ # 补全脚本关键操作是把~/.cliany/bin加进 shell 的 PATH。注意不要在目录里直接堆一堆 foo.py而是统一做一个入口然后在 bin 里放软链或者同名脚本。我的做法是每个模块就是一个cb-模块名脚本再用一个总的cb命令做分发。比如~/.cliany/bin/cb ~/.cliany/bin/cb-rename ~/.cliany/bin/cb-qr ~/.cliany/bin/cb-todocb作为统一入口会解析第一个子命令然后 dispatch 到具体模块。你也可以直接把所有命令都写成cb xxx的形式不用做多个脚本。但我的实测感受是独立脚本调试更快、报错定位更清晰因为每个脚本的职责非常单一。而cb这个壳命令只负责参数路由和帮助信息展示。PATH 这块有一个特别容易犯的错千万不要把整个~/.cliany放进去而是只放bin子目录。否则lib、config里的文件也可能被执行到而且 Python 的包扫描会出问题。另外建议把~/.cliany/bin放在 PATH 靠前位置保证自定义命令优先于系统命令这个后面会在常见问题里再讲。2.2 通用函数库日志、错误处理、依赖检测模块之间的公共逻辑如果不抽出来代码会变成一团乱麻。我先是把日志函数统一封装因为命令行工具最影响体验的细节之一就是输出。好的输出能让你一眼看出命令成功了、失败了、失败在哪一步。我封装了一组很简单的函数# lib/common.py 片段 import sys, os, datetime LOG_LEVEL os.getenv(CB_LOG_LEVEL, INFO) def log(msg, levelINFO): ts datetime.datetime.now().strftime(%H:%M:%S) color {INFO: \033[0;32m, WARN: \033[0;33m, ERROR: \033[0;31m}.get(level, \033[0m) print(f{color}[{ts}][{level}]\033[0m {msg})这个函数看起来简陋但实际使用率极高。它会为日志加上时间戳和颜色WARN黄色、ERROR红色、INFO绿色。为什么要带时间戳因为在后台跑任务或者查看日志时时间信息能帮你判断执行效率。为什么颜色要单独控制因为在管道输出场景下你可能不需要颜色所以我在公共函数里做了一个环境变量开关CB_NO_COLOR1时就会去掉颜色码。错误处理是另一个公共能力。经验是不要把所有错误都交给 Python 的 traceback 去展示普通用户看到一屏堆栈会直接懵掉。更好的做法是统一die()函数输出一句话错误说明附带提示排查命令然后exit(1)。def die(msg, hint): log(msg, ERROR) if hint: print(f提示: {hint}, filesys.stderr) sys.exit(1)最后是依赖检测。CLI-Anything 大量依赖第三方命令行工具比如fzf、qrencode、tesseract、jq。这些工具不一定每台机器都装了。所以我写了一个require()方法在模块启动时统一检测核心依赖。def require(*cmds): missing [c for c in cmds if not shutil.which(c)] if missing: die(f缺少依赖: {, .join(missing)}, 请先用 brew/apt 安装对应工具) return True现在任何一个模块跑起来第一件事就是把缺失依赖暴露出来而不是执行到一半才炸。这几个公共函数解决了输出、退出、依赖三件套后续写模块几乎都要用到省下来的重复代码量相当可观。2.3 补全系统与帮助规范对一个命令工具箱来说命令有没有补全体验差距巨大。刚开始我用纯argparse它能自动生成--help但没法做子命令的 Tab 补全。后来我直接用argcomplete配置简单、效果也很稳。# 在 ~/.cliany/bin/cb 里 eval $(register-python-argcomplete cb)argcomplete 可以按 argparse 的解析规则自动补全子命令和参数。比如输入cb rTAB它会自动补全成cb rename输入cb rename --reTAB会补成--replace。这个体验和使用系统的git TAB非常接近。至于帮助信息我的规范是每个模块必须支持-h/--help并且帮助文本第一行必须是一句话功能描述然后是一到两行示例。原因是命令行工具最容易忘的不是怎么用而是它到底能干啥。帮助系统就是命令工具箱的记忆索引写得清楚才敢放心地让一堆命令沉淀下来。3. 实操过程6 个高频场景的实现这一部分挑选了 6 个我认为最有代表性、也最常用到的场景从需求分析到命令实现完整过一遍。每个场景的代码都不是特别复杂但都有一些值得注意的设计取舍。3.1 批量重命名带正则和演练模式批量重命名是每个人的刚需但简单的重命名工具往往不支持正则支持正则的图形工具又得开软件。用 CLI 做的思路其实很直接接收一个正则表达式匹配原文件名替换成新模式。关键的设计点是演练模式。# cb-rename 核心逻辑简化 import re, os, sys, argparse from lib.common import require, log, die def rename_files(directory, pattern, replacement, dry_runTrue): for filename in sorted(os.listdir(directory)): new_name re.sub(pattern, replacement, filename) if new_name filename: continue old_path os.path.join(directory, filename) new_path os.path.join(directory, new_name) if dry_run: log(f{filename} - {new_name}) else: os.rename(old_path, new_path) log(f已重命名: {filename} - {new_name}) if __name__ __main__: parser argparse.ArgumentParser(description批量重命名工具) parser.add_argument(--pattern, requiredTrue, help正则匹配模式) parser.add_argument(--replace, requiredTrue, help替换为) parser.add_argument(--dir, default., help目标目录) parser.add_argument(--dry-run, actionstore_true, help演练模式只输出不执行) args parser.parse_args() rename_files(args.dir, args.pattern, args.replace, dry_runargs.dry_run)在使用上cb rename --pattern IMG_(\d).JPG --replace photo_$1.jpg --dry-run会先列出所有将要发生的变更确认无误后去掉--dry-run再真正执行。这一点极其重要因为重命名的反悔成本很高尤其当文件名已经写入到别的系统里时。演练模式补全了最后的安全感实测下来每次批量操作我几乎都会先跑一遍演练。这里还可以加入一个很实用的参数--undo-file。在真正执行时把原来的文件名和新文件名写入一个映射文件万一改错了可以反向恢复。虽然大多数场景用不到但真遇到事故时这个文件就是救命稻草。3.2 语音提醒与定时任务很多人用 GUI 日历软件管理提醒但既然所有事都在终端里做为什么不把提醒也变成命令我的模块cb remind做了一件很简单的事接收 时间 内容到点后利用系统能力弹通知。macOS 可以用osascript弹原生提醒Linux 上可以用notify-send系统能力有差异需要做分支。def notify(title, message): platform sys.platform if platform darwin: os.system(fosascript -e \display notification {message} with title {title}\) elif platform linux: os.system(fnotify-send {title} {message}) else: print(f[提醒] {title}: {message})这个模块真正麻烦的是定时两字。我的方案不需要持续运行的守护进程而是利用系统自带的定时能力。macOS 用launchdLinux 用cron两条命令把延迟任务注册进去。比如 25 分钟后的番茄钟休息提醒cb remind --after 25m 站起来走走喝口水它会计算好目标时间写一个一次性脚本然后交给launchd/at去执行。之所以不自己写常驻进程是为了避免为了一个提醒功能额外占用资源也避免了进程管理崩溃的问题。系统自带工具虽然老但极其稳。3.3 OCR 图片文字提取OCR 是另一个高频需求。图形工具普遍臃肿命令行方案反而干净利落。底层我用tesseract但直接调用它的问题在于参数复杂、输出路径混乱、语言包管理麻烦。CLI-Anything 包了一层让 把一张图变成可复制的文字 这件事变成一条命令。cb ocr --lang chi_simeng --output stdout screenshot.png核心就是先检测 tesseract 依赖然后拼接命令行参数最后把结果输出到 stdout 或写入一个文件。这里的--lang chi_simeng是一个很有讲究的参数,因为很多截图里既有中文又有英文只选一种语言会导致另一部分内容识别效果变差。而 tesseract 支持语言包叠加用连接即可。需要注意截图类文字识别之前最好先对图片做一次预处理适当放大、转灰度识别率会明显提升。我在模块里默认加了这一步用 Python 的 PIL 库将图片统一处理成高分辨率的灰度图再丢给 tesseract实测下来识别准确率至少能提升 20%。3.4 二维码生成与应用二维码在生活和工作里无处不在但没有一个跨平台的命令行工具能把生成二维码这件事从打开网页工具变成一行命令完成。qrencode这个工具很强唯一的毛病是默认输出是一堆终端乱码。我用模块对它做了两层封装一是自动选择输出格式二是生成后自动用系统默认图片查看器打开。def make_qr(data, outputNone): output output or f/tmp/cb_qr_{int(time.time())}.png os.system(fqrencode -o {output} -s 12 -m 2 {data}) print(f二维码已生成: {output}) if sys.platform darwin: os.system(fopen {output}) elif sys.platform.startswith(linux): os.system(fxdg-open {output}) return output这里我故意把输出路径放在/tmp原因很实际二维码多数是一次性使用看完即焚没必要污染工作目录。参数-s 12 -m 2分别表示每模块的像素大小和外边距12 是大多数扫码软件的最佳识别尺寸。如果生成的二维码太小扫码可能会出现识别困难如果太大图片又浪费。实测 12 这个值在绝大多数屏幕上表现良好。如果你突然要分享一段 WiFi 密码也可以用这个命令直接生成一个标准的 WiFi 二维码比如cb qr WIFI:T:WPA;S:MySSID;P:MyPassword;;手机一扫就能连上这个技巧在公司临时访客网络里特别实用。3.5 一键初始化 Git 仓库Git 操作本身就是在命令行里完成的但初始化一个新仓库并推到远端这件事依然繁琐。git init、git add、git commit、创建远端仓库、推送五个步骤每次手动输入还会忘掉远端的地址格式。所以cb gitinit这个模块把整个流程收敛成一条命令并且每次都保持同样的提交信息和分支命名习惯。cb gitinit feat: initial commit它做的事按顺序是检查当前目录是否已经是仓库是则报错避免重复初始化git init -b main默认主分支用main这是现在的行业惯例生成.gitignore没有则从模板复制一份git add -A git commit -m $1解析远端地址如果配置了 GitHub token则直接调用 API 创建同名仓库并git remote add origin最后推送这个模块里最需要小心的一步是第 5 步。调用 GitHub API 创建仓库时需要 token而 token 的泄露风险非常大。我的做法是从环境变量里读取绝不写在代码或者配置文件里。判断方式是用户在首次运行时会有一个提示把 token 写入~/.cliany/config/env然后所有模块统一从这个地方加载密钥。这对局域安全性是一个很好的兜底习惯密钥集中存放统一管理代码里不出现明文。3.6 Todo 列表的同步与归档最后一个场景是 Todo 管理。Todo 工具千千万但我最终选择用文本文件 简单脚本。原因是文本文件是通用格式可以同步到任意系统可以 grep可以写进 git 版本管理还能配合 fzf 做交互搜索。这在数据可移植性上有着压倒性的优势。cb todo add 写周报 cb todo list cb todo done 1 cb todo archive实现上Todo 文件就是纯文本每行一行ID | 状态 | 创建时间 | 内容。状态字段用[ ]表示未完成[x]表示已完成。列表展示时我直接接管道给fzf按下 Tab 多选然后一键标记为完成。所有操作都不需要数据库备份也就是复制一个文件。数据在自己手里任何 Todo 软件倒闭都不影响你的任务列表。这个模块还提供了一个附加功能每天凌晨用launchd自动跑一次归档把已完成超过三天的条目挪到archive.log让主文件保持清爽。因为 Todo 文件太长了以后fzf 的搜索体验会下降定期归档是保持工具好用的必要手段。4. 常见问题与排查技巧实录CLI-Anything 从搭框架到现在我踩过的坑一点都不比功能代码少。这类命令行工具常见的问题很多是隐藏的不遇到根本不会想到。我把最有代表性的几个问题记下来希望能帮你少走一些弯路。4.1 alias 失效引号展开的陷阱刚开始我为了方便把一些复杂命令写成 alias比如alias renamepython3 ~/.cliany/bin/cb-rename用了一阵子发现某些带单引号参数的场景老是报错比如cb rename --pattern IMG_(\d).JPG。原因在于 alias 在 bash/zsh 里的展开机制以及引号嵌套的冲突。alias 展开是纯文本替换最容易在参数里包含特殊字符时出问题。把 alias 换成函数或者直接写脚本的话参数传递走的是正经的 argv不会出现二次展开问题。后来我把所有命令的入口都收敛成真正的脚本不再用 alias。这个改动让我彻底摆脱了命令时好时坏的困扰。4.2 命令名冲突与 PATH 污染所有自定义命令最怕和系统命令重名。如果你写了一个脚本叫cd那系统里所有cd行为都会被劫持轻则功能异常重则 shell 直接没法用。这是新手最容易踩的坑之一。我的解决方案是固定前缀cb这样既不和主流系统工具冲突也让命令体系有辨识度。还有一个隐藏坑PATH 污染。如果把~/.cliany整个放进 PATH而目录下又恰好有一个.env文件或者lib子目录里的脚本你在终端输入某些缩写时就可能执行到完全意想不到的脚本。所以必须只把bin放进去。另外建议在 PATH 里把自己目录放在靠前位置但同时要保留系统路径的搜索能力。否则某些命令会被自己的脚本遮蔽。4.3 依赖检查的重要性我在第一个版本里很多模块完全没有依赖检测。比如 OCR 模块如果机器没装 tesseractPython 脚本不会立刻报错而是会抛出一个底层错误或者更糟静默失败然后输出一个空文件。这种问题排查成本极高因为错误信息和真正的原因之间隔了一层语言绑定。现在每个模块开头统一调用require(tesseract, qrencode)这类函数缺什么明确告诉你缺什么还附带安装提示。这一行的价值远超你的想象相当于把潜在半天的排查时间变成一秒定位。4.4 跨平台差异我的主力环境是 macOS但不少脚本会跑在 Linux 服务器上。跨平台问题最集中的体现在通知、剪贴板、图片打开方式三个模块。通知上面讲过要用osascript/notify-send分支。剪贴板也一样macOS 是pbcopyLinux 是xclip。图片打开方式则是open/xdg-open。一个变量把这些差异都管好的方法是在公共库里写一个统一的clipboard函数下面再分平台实现调用。这样业务模块不需要关心自己跑在什么系统上只管调用统一接口就行。4.5 换机部署一次备份全带走命令行工具箱最核心的资产不是代码而是配置和数据。换新机器时如果只拷贝脚本而忘记配置很多命令启动后连 API token 都没有体验会非常割裂。我的做法是把~/.cliany整个目录变成 git 仓库同时把密钥类文件用.gitignore排除。新机器上操作三步git clone代码、执行一次安装脚本做软链、再把密钥环境变量补充进去。整个过程五分钟不到。数据文件比如 Todo 列表、账本因为格式都是纯文本也跟着仓库走天然同步。这套流程我已经在三四台机器上跑过非常稳定。5. 进阶扩展让工具更贴合个人习惯5.1 从单条命令到组合工作流CLI-Anything 真正发挥威力是在命令可以互相嵌套或组合的时候。举例来说我有一个小工作流是截屏转文字并写入笔记它由几步组成先截屏再用 OCR 模块识别最后追加到今天的日记文件。这些操作单独拆开都不复杂但当它们被封装成一个新模块cb note-screenshot时你就有了一个高频操作的一键版本。function cb_note_screenshot() { local img$(screencapture -x /tmp/cb_shot.png echo /tmp/cb_shot.png) local text$(cb ocr --lang chi_simeng $img) echo - $text ~/notes/$(date %Y-%m-%d).md echo 已写入笔记: $text }这种组合正是 CLI 相比 GUI 的核心优势图形界面里的每个操作都是孤岛命令行里的一切都可以自由联通。用了一段时间后你会发现自己开始有意识地把操作拆成可重用的积木再拼装成新的工作流。5.2 CLI 与 GUI 的合理边界虽然我自称万物皆可命令行但实际使用中还是要诚实面对 CLI 的边界。某些场景它就是不适合命令行比如复杂图片精修、视频剪辑、可视化图表分析。强扭的瓜不甜CLI 适合的是规则明确、可批量、可重复、文本化的操作GUI 适合的是探索性强、需要视觉反馈、需要低上手成本的操作。我个人画了一条清晰的线凡是需要看才能做决定的事交给 GUI凡是不看也能做决定的事全部尝试 CLI 化。这条线帮我避免陷入为 CLI 而 CLI的偏执也保证了效率最大化。把精力放在真正能自动化的事情上让命令行工具箱保持精简、高效才是长期可持续的做法。5.3 配置化与密钥管理的心得最后再分享一个帮我避免多次事故的小技巧所有模块的配置项包括 API 密钥、默认路径、语言参数都统一放到~/.cliany/config/env文件里然后在公共库加载。这不只是方便管理更重要的是避免把密钥提交进 git 仓库。我在初始版本里吃过一次教训把 GitHub token 直接写在了脚本里后来推仓库时差点泄露还好及时发现。现在所有 token 一律从环境变量读取脚本里只有变量名没有任何秘密。针对这个文件我还加了一个简单的加密选项使用系统 keychain 或者 gpg 做对称加密启动时手动解锁。代价是每次新开 shell 得输一次密码但换来的是更高的安全感。对不同需求的人可以根据自己的风险偏好选择明文加权限控制或者加密存储。6. 用了一阵子之后的体验整理CLI-Anything 从搭建到打磨陆陆续续用了几个月。我最大的感受是它像是一个一直在长身体的工具随着你遇到的新场景、新痛点越来越多工具箱也会越来越丰富。它并不需要一次做到完美而是随着使用慢慢沉淀出真正高频的命令淘汰掉低频的命令。对我而言这种让工具跟随自己的习惯生长的感觉远比安装一个功能全面但处处不顺手的商业软件要舒服得多。如果你也打算搭一套自己的命令行工具箱我的建议是别从大而全的框架开始。先写下你最常做的 10 个操作挑出其中重复性最强、规则最明确的两三个用 30 分钟实现成命令。等跑顺了再慢慢补齐其他模块。这套东西不难难的是坚持在命令行里完成日常小事并把它们沉淀成可持续复用的资产。
返回列表