CLI-Anything:用命令行把重复操作变成一条命令

发布时间:2026/9/28 13:38:54
CLI-Anything:用命令行把重复操作变成一条命令 去年年底我整理硬盘面对电脑里一堆图片管理、压缩、批量改名的工具忽然意识到一个尴尬的事实日常处理这些事务最花时间的不是工具本身而是“打开→点菜单→拖文件→确认→关闭”的循环。尤其像把散落多个文件夹里的一千多张照片按拍摄日期归档这种活用图形界面做基本等于半天的重复劳动中途手抖一下还可能把分类全弄乱。也是在那段时间我给自己立了一个目标凡是重复超过三次的操作全部尝试做成命令行。这个目标被我命名为 CLI-Anything它不是一个具体软件而是一套把“一切可固定规则的任务”命令行化的工作方法。这篇内容不是讲某个特定框架的教程而是完整记录我从需求判定、工具选型、参数设计到真实使用中踩坑的全过程。如果你是一个经常和文件、数据、服务器打交道的程序员、运维或数据分析师又或者你只是受够了图形界面里的“下一步、下一步”这篇文章会给你一套可以直接抄走的思路和代码骨架。1. 为什么我非要让“一切操作”都走命令行先澄清一点我并不讨厌 GUI。图形界面在可视化场景里优势巨大给不懂技术的人用给需要看缩略图的场景用都比命令行强得多。但问题在于GUI 天然不适合“重复性任务”。每次点击鼠标的位置可能有偏差操作步骤无法被版本化管理更没法塞进定时任务和自动化脚本里。而重复任务的本质要求是确定性和可追溯性——这恰好是命令行最擅长的事。命令行本质上就是“文本输入 文本输出”的黑盒。它简单到可以把输入输出用管道串起来又严格到每一步都能被脚本复现。这种特性让它成为自动化和远程操作的最佳接口。CLI-Anything 的核心思路其实不复杂凡是固定输入、固定规则、重复执行的流程都优先考虑做成一条命令做不到的再退回图形界面。我发现很多人用命令行失败不是因为不会敲命令而是因为跨过了中间层。他们要么只敲一次性命令用完就忘要么一上来就想写一个完美的工具结果被需求变化和边界情况劝退。我自己后来的习惯是分三步走先用一条临时命令跑通一个具体文件确定规则没问题再把它泛化成脚本加上参数最后才考虑做成“像样”的 CLI 工具。这样每层都验证过了不会一上来就陷入设计泥潭。还有一点很重要CLI-Anything 真正适合的人是那些自己产生重复操作、自己能定义规则的人。它的价值不在于“敲命令很酷”而在于把人的精力从“怎么点”转移到“规则是什么”。规则一旦落地后面所有的重复都变成机器的责任。2. 动手前的判定卡四类场景四种做法在把任何东西命令行化之前我会先问自己两个问题这个操作有没有固定的输入和输出这个操作我一个月会不会重复超过三次两个答案都是“是”才值得投入。否则一次性的、需要大量视觉判断的任务比如从一堆照片里挑出最好看的那张强行做成命令行反而是低效的。通过判定之后我把遇到的场景分成四类处理方式完全不同。场景类型典型例子推荐做法本身有 CLI 工具只是被图形界面包着压缩软件、音视频处理工具、Git翻--help直接调用底层命令程序提供公开 API 或网络接口数据平台、云服务、内容管理系统用curl或写一个 requests 脚本封装桌面应用没有 CLI但有系统级自动化接口macOS 备忘录、Office 系列AppleScript、Windows COM、辅助功能 API纯手工图形操作或网页操作浏览器后台、无接口的设计类工具键盘鼠标模拟或浏览器自动化场景 A 是最容易被忽略的。很多软件安装包里藏着一个命令行可执行文件只是默认不在 PATH 里。压缩软件、截图工具、甚至一些 Electron 应用都是这样。我先翻安装目录找有没有同名cli、bin之类的入口再决定要不要自己封装。不要上来就写自动化优先复用已有的命令行能力。场景 B 的特点是“输入输出都是数据”这种最适合做脚本。哪怕今天只需要查一条记录一旦封装成命令下次配合cron就能变成定期任务。我一般用 Python 的 requests 或系统自带的 curl 起手关键是保持一个统一的输出结构方便后面接入 jq 之类的处理工具。场景 C 和场景 D 才是我这篇里重点聊的“硬包装”。这类操作往往写在 GUI 里但并非没有程序化入口。AppleScript、COM 接口、辅助功能权限都能把 GUI 操作翻译成可控命令。唯一的原则是能用接口解决的不要用鼠标模拟解决必须用鼠标模拟的要做超时和状态检查避免脚本在无人看管时卡死。最后的忠告是不要为了 CLI 而 CLI。如果这个操作每个月只做一次或者做的时候需要大量主观判断——比如挑选主色调——那图形界面依然是更好的选择。CLI-Anything 不是要消灭 GUI而是要把 GUI 从“唯一选项”变成“保留选项”。3. 现实案例把“照片按日期归档”做成一条命令拿我最常做的“照片归档”当例子完整走一遍从需求到命令的过程。需求背景很朴素相机和手机里的照片混在好几个目录里文件名是IMG_20240105_123456.jpg这种夹着截图、网图。我希望按拍摄日期归档到固定目录结构例如2024-01、2024-02这样。用图片管理软件一个个拖动几百张还能忍上千张就会疯掉。这个需求符合 CLI-Anything 的全部特征规则固定按 EXIF 拍摄时间归到年月目录、重复度高每个月都要整理一次、可回滚先复制再删除而不是直接移动。设计这条命令时我定了几条规则默认先跑--dry-run只打印计划不实际动文件默认复制而不是移动防止目录结构判断错导致文件丢失无法读取日期的文件要明确提示“跳过”而不是静默处理。核心脚本我用 Python 写依赖只有Pillow用来读取 EXIF 信息。完整骨架如下#!/usr/bin/env python3 import argparse import shutil from pathlib import Path from datetime import datetime try: from PIL import Image except ImportError: Image None def get_shoot_date(path): 读取照片拍摄日期返回 datetime 对象无法读取时返回 None。 if Image is None: return None try: with Image.open(path) as img: exif img._getexif() or {} # 36867 是 DateTimeOriginal 标签 date_str exif.get(36867) or exif.get(306) if date_str: return datetime.strptime(date_str, %Y:%m:%d %H:%M:%S) except Exception: pass return None def main(): parser argparse.ArgumentParser(description按拍摄日期归档照片) parser.add_argument(--source, requiredTrue, help源目录) parser.add_argument(--target, requiredTrue, help目标目录) parser.add_argument(--copy, actionstore_true, help复制而非移动文件) parser.add_argument(--dry-run, actionstore_true, help只打印计划不实际执行) args parser.parse_args() source Path(args.source) target Path(args.target) target.mkdir(parentsTrue, exist_okTrue) for img_path in sorted(source.rglob(*.jpg)): date get_shoot_date(img_path) if date is None: print(f[skip] 无法读取拍摄日期: {img_path}) continue folder target / f{date.year}-{date.month:02d} folder.mkdir(parentsTrue, exist_okTrue) dest folder / img_path.name if args.dry_run: action copy if args.copy else move print(f[plan] {action} {img_path} - {dest}) continue if args.copy: shutil.copy2(img_path, dest) else: shutil.move(str(img_path), str(dest)) print(f[done] {img_path} - {dest}) if __name__ __main__: main()运行效果大概是这样的python photo_archive.py --source ~/Downloads --target ~/Photos --dry-run [plan] copy /Users/me/Downloads/IMG_20240105_123456.jpg - /Users/me/Photos/2024-01/IMG_20240105_123456.jpg [plan] copy /Users/me/Downloads/IMG_20230211_091200.jpg - /Users/me/Photos/2023-02/IMG_20230211_091200.jpg [skip] 无法读取拍摄日期: /Users/me/Downloads/screenshot_001.png你看整个命令跑一遍不到十秒而同样的操作在 GUI 里至少要半小时。--dry-run这个参数是关键它能让我在真正移动文件之前先用眼睛扫一遍计划是否正确。确认无误后再把参数换成--copy或者直接移动风险就小很多。4. 没有CLI的桌面程序怎么“硬包装”成命令行不是所有工具都有现成的命令行入口。遇到这类桌面程序我按“接口优先、模拟兜底”的顺序处理。先看系统级自动化接口。在 macOS 上AppleScript 是我最常用的手段。比如给备忘录加一条草稿一条命令就够osascript -e tell application Notes to make new note with properties {body:CLI-Anything 测试}系统会自动唤起应用并执行动作。这类能力比大多数人想象中强大很多 Mac 应用都实现了 AppleScript 字典相当于给 GUI 开了一个程序化后门。在 Windows 上COM 接口扮演类似角色。以 Excel 为例PowerShell 可以直接创建一个工作簿并写入内容$excel New-Object -ComObject Excel.Application $excel.Visible $false $wb $excel.Workbooks.Add() $wb.Sheets.Item(1).Cells.Item(1, 1) CLI-Anything $wb.SaveAs(C:\tmp\test.xlsx) $wb.Close() $excel.Quit()这种方式足够稳定因为走的是官方公开接口不是像素级的模拟操作。缺点是必须先确认目标应用是否注册了 COM 或支持 AppleScript而且权限控制比较严格第一次运行会要求授权。再一种思路是查安装目录。很多看起来只有 GUI 的软件实际在bin或resources目录里藏了命令行可执行文件。Electron 类应用尤其如此主程序常常附带一些可调用的内部命令。我先翻一遍安装目录再看看有没有--help输出往往能发现意外惊喜。最后才是键盘鼠标模拟。Python 的pyautogui可以定位、点击、输入但它属于“最不稳定的后手”。原因很简单GUI 坐标受窗口位置、缩放比例、系统主题影响今天能跑通明天换个分辨率就失灵。如果实在要用我坚持两个原则第一定位使用窗口相对坐标而不是屏幕绝对坐标第二每一步操作后加上条件等待确认界面元素出现再继续而不是盲目sleep。这个“先接口、后命令、再模拟”的优先级能帮你省下很多折腾时间。反过来一上来就搞鼠标模拟往往会被各种偶发问题拖进泥潭。5. 把CLI做得顺手参数、输出、退出码的三条设计红线CLI-Anything 的项目多了以后我逐渐总结出几条设计红线。遵守它们工具自己会变得“顺手”不遵守命令用起来总有一种别扭感。第一个是参数设计。统一采用“主命令 选项”结构所有影响行为的开关都放在选项里所有被处理的对象都放在位置参数里。选项命名要一致--dry-run、--yes、--json这类通用选项在全部脚本里保持同名同义。安全默认值优先于便捷默认值破坏性操作默认不执行必须显式传--yes或对应参数才执行。Git 的哲学就很适合借鉴——先查询后写入先预览后提交。第二个是输出规范。我严格区分 stdout 和 stderr正常结果输出到 stdout比如归档后的文件路径日志和警告输出到 stderr。这样管道和重定向才不会污染真正的输出结果。同样重要的是检测当前是否为终端tty只有终端条件下才输出彩色 ANSI 转义符。在 Python 里可以这样import sys use_color sys.stdout.isatty() if use_color: print(\033[32m[done]\033[0m 处理完成) else: print([done] 处理完成)如果忽略了这一步脚本输出被重定向到日志文件时里面会全是^[[32m之类的转义垃圾排查问题时非常痛苦。更进一步如果命令的输出要喂给别的程序最好提供一个--json选项输出稳定的结构化数据。第三个是退出码约定。Shell 的if判断依赖退出码0 代表成功非 0 代表失败。Python 脚本里一旦发生未处理异常默认退出码是 1但业务上的“部分成功”建议用 2 表达。比如照片归档时假如有 10 张照片因为缺 EXIF 被跳过而其他都成功这个“非致命但没完全成功”的状态返回 2 比返回 0 更诚实。在脚本结尾必须显式sys.exit(0)或sys.exit(2)不能依赖默认行为。生态选型上不同语言的 CLI 库差异也很大。我自己的选择参考如下语言常用库适合场景说明Pythonargparse小型脚本和内部工具标准库自带零依赖Pythonclick中大型命令行工具参数组合、命令分组很方便Node.jscommander.js前端工具链与前端生态配合好Gocobra需要单文件分发的高性能工具云原生工具基本都靠它我的经验是个人内部工具优先用 argparse团队共享工具优先用 click 或 cobra因为它直接影响--help的呈现质量。CLI 做得顺手不靠文档数量而靠命令本身自解释——参数名、选项默认值、输出信息都要直接表达意图。6. 用了半年后我总结出的CLI避坑清单把一批操作 CLI 化之后真正让项目稳定运行的不是初期的顺利而是后期对坑的提前防守。下面这六条我基本每次都会踩一次现在先写给你避开。第一中文文件名乱码和编码问题。处理大量中文文件名的文件时Python 的os.path和字符串拼接很容易出问题。我的经验是一律用pathlib.Path它内部会处理不同平台的路径分隔符和编码差异。第二路径带空格导致命令拆分错误。这个问题隐蔽在自动化脚本里。用系统命令时比如在 Python 里调subprocess.run()永远不要把路径直接拼进字符串而要用参数列表传递# 错误 subprocess.run(fls -l {file_path}) # 正确 subprocess.run([ls, -l, str(file_path)])第三重定向输出里出现乱码转义。这就是上面提到的 ANSI 问题。解决办法已经写过只在isatty()为真时开颜色千万别图省事一直开。第四定时任务跑脚本时提示“找不到命令”。很多人写脚本开头用#!/usr/bin/env python3在终端里可以运行但放进crontab就报错因为 cron 的最小环境里 PATH 可能没有 Python 安装目录。对策是脚本开头显式指定解释器绝对路径或者用env变体但配合完整 PATH 设置。第五交互式提示在自动化场景卡死。如果你写的脚本里有input(确认吗?)那在无人值守的定时任务里它会永远等下去。这就是为什么我在所有脚本里坚持预留--yes参数先判断是否非交互模式是就直接按“确认”处理。设计原则是任何交互提示都必须有对应的非交互开关。第六UI 自动化脚本过几天就不能用了。这个坑我踩得最狠。点击坐标是屏幕级的一旦改了显示器分辨率或者加了外接屏位置全变。更好的思路是按窗口相对坐标定位并且在点击前用图像识别或辅助功能 API 确认目标元素存在。GUI 自动化的天然脆弱性没法完全消除所以它永远是我最后的选择。还有个更大的经验值CLI 化不等于“一次性脚本”。三个月后没人维护的工具反而会成为新的维护负担。我给自己定了一个策略所有小工具统一放在~/scripts目录每个工具在头部注释里写明参数、用途和最后验证日期每半年集中清理一次。这个习惯让我的命令行工具箱始终干净也让我能放心地把重复劳动交给它们。写了这么多我最深的体会是CLI-Anything 真正值钱的地方不在于命令本身而在于它强迫我把每次重复操作都提炼成“输入→规则→输出”的思考模型。现在我电脑里有几十个小脚本每个都不超过 200 行却把每个月最大量的重复劳动压缩成了周末下午的一条流水线。如果你也想照着做我的建议是别贪多就从本周你最想吐槽的那个重复操作开始。先让它--dry-run能跑通再走完一次真实执行你很快就能找到那种“黑盒替我干活”的感觉。