
最近我花了两天时间把一个思考很久的工具链彻底跑通了。先说说我一直以来的想法作为一个重度命令行用户我电脑里的大部分操作其实都能在终端里完成。但总有一些事情原本除了打开某个软件、点几个按钮、来回切换窗口之外真的别无他法。比如把一份网页保存成干净的文字稿、批量整理图片尺寸、把剪贴板内容发给某个团队协作工具、定时抓取某个页面变化……这些东西每做一次都要重复一遍手工流程次数多了就特别想吐槽为什么不能有个统一的命令行入口把所有这些事都串起来。CLI-Anything 就是冲着这个痛点来的。它不是某个具体的脚本而是一套“把所有常见操作统一暴露成命令行调用”的思路和落地框架。你可以把它理解成给各种日常工作流装一个操作面板原来你需要在不同的软件、网页、窗口之间来回折腾的事情现在只需要在终端里敲一行命令。这篇文章不打算写成正式的架构文档我就按自己实操的顺序把设计思路、核心功能、踩过的坑、以及后续能怎么扩展一步步聊清楚。不管你是写脚本的老手还是刚接触命令行的新人只要对“少点鼠标、多敲键盘”有兴趣这篇都能给你一些可以直接抄作业的东西。1. 为什么非要“万物皆可命令行”先聊一个比较根本的问题都已经有图形界面了那么多软件做得越来越好看为什么还要把东西往命令行里塞我的答案很简单因为命令行是唯一一种能让“操作本身”被记录、被复用、被自动触发的方式。你在图形界面里点二十次鼠标完成一次批量处理下一次还要再点二十次。但如果你把这二十次点鼠标变成一条命令下次执行只需要敲一次回车。更进一步这条命令可以被定时任务调用、可以被另一个脚本调用、可以放在服务器上无人值守地跑这个时候你得到的不只是“省事”而是一个可以被组合、被编排、被纳入更大流程的积木。CLI-Anything 想充当的角色就是这块积木的生产机器。拿一个生活化的类比来说图形界面像一个有很多按钮的遥控器每个按钮都对应一个功能但你得记得哪个按钮在哪、按多少次、按完等多久。命令行更像是一张写好的清单你把要做的步骤写清楚剩下的事情就是执行。CLI-Anything 做的事情就是帮我把散落在各种软件里的“按钮”找出来统一编成一张好用的清单。具体到我自己的使用场景有三类需求最典型重复性劳动。比如每天都要把某几个来源的内容整理成同一格式的文件手工操作耗时且容易漏步骤。跨工具的数据搬运。A 软件里的内容要经过处理之后扔进 B 软件中间还有转码、清洗之类的环节纯手工做效率极低。无人值守的操作。有些任务需要定期执行、定时触发图形界面很难支持这种“到点自动跑”的用法。这三类需求背后其实都指向同一个结论凡是过程可以被规则描述的就应该被写成命令。CLI-Anything 的“Anything”就体现在这里——它不是只针对某一类工具的封装而是把一切规则明确、步骤固定的操作都纳入到同一套命令行体系里。适用人群也比较清晰。如果你平时的日常工作离不开终端或者你负责维护一些自动化流程又或者你只是烦透了重复点击CLI-Anything 的思路都能帮你省下不少时间。反过来如果你对命令行的认知还停留在“黑框框很吓人”那这篇里我也会把每一步都拆得很细照做基本不会翻车。2. 核心设计一张映射表和三条管道CLI-Anything 真正跑起来之后内部结构其实不复杂。我用一句话概括就是一张映射表加三条管道。映射表负责“从命令到动作”的翻译三条管道负责把动作执行过程中的输入、输出和错误处理串起来。所谓映射表本质上就是一个配置文件。它记录了每一条你定义的命令背后实际要调用什么程序、传什么参数、用什么方式解析结果。CLI-Anything 做的事非常简单拿到终端里输入的指令在映射表里找到对应的执行计划然后按计划干活。我见过不少人一上来就想着写一个很大的框架支持动态加载插件、支持远程调用、支持可视化配置界面。但我的经验是第一版千万不要做这么多。CLI-Anything 的核心价值是“用简单的声明式配置解决 80% 的重复操作需求”那些复杂功能后续可以慢慢加但如果一开始就把设计搞复杂很可能写到一半就放弃。我实际使用的配置结构大概是这样的commands: save_page: description: 把URL内容保存为干净的Markdown文件 steps: - fetch: { url: $url } - extract: { selector: article } - save: { path: ./output/$title.md } options: timeout: 30这只是一个示意不同实现方式可以有很多变体。但核心思想是一样的命令名、参数、执行步骤、超时这些信息都写在一个人类可读的配置里而不是硬编码在代码中。这样做的好处很多——别人接手你的工具链时打开配置文件就能看懂整个逻辑不需要去翻源码。三条管道分别是输入管道、输出管道和错误管道。很多人做类似工具的时候只关注“命令能不能执行成功”忽略了输入输出和错误的标准化结果就是每个命令的交互方式都不一样用起来非常割裂。我的做法是所有命令统一从标准输入读取参数统一输出结构化结果统一把错误打包成固定的格式。这样上层不管是人敲命令还是脚本调用体验都是一致的。在设计的时候我参考了一个很成熟的思路把命令行程序当作函数来使用。入参是标准化的返回值是结构化的异常是显式抛出的。CLI-Anything 只是把这个思路推广到任意操作上让原本不是命令行程序的东西也表现得像一个干净的命令行函数。还有一个细节容易被忽视目录和路径的处理。CLI-Anything 应该在哪个目录下执行相对路径怎么解析我在实际使用中吃过不少亏后来定了一条规矩所有配置里的相对路径都相对于配置文件所在目录而不是相对于当前终端所在目录。这样可以避免同一个命令在不同目录下跑出完全不同的结果。稍后我会在踩坑部分再展开讲。3. 从零到一安装、初始化、写第一个命令这一节我按实操顺序来每个步骤都给出可以直接照做的内容。CLI-Anything 本身可以基于很多语言实现我选的是 Python 加 Click 库原因是生态成熟、写起来快、依赖管理省心。如果你更熟悉 Node.js 或 Go用类似的思路也可以。第一步是安装基础环境。我假设你已经装了 Python 3.9 以上版本然后用虚拟环境隔离依赖mkdir cli-anything cd cli-anything python3 -m venv .venv source .venv/bin/activate pip install click pyyaml requests beautifulsoup4这几条命令做完之后你就有了一整套解析配置、发 HTTP 请求、解析 HTML 的依赖。CLI-Anything 的核心程序我写成了一个单独的入口文件结构非常简单读取配置文件、解析出命令表、然后将终端输入的参数匹配到对应命令上。接下来是初始化配置。我习惯先在项目根目录放一个commands.yaml里面定义一两个最简单的命令跑通了再继续加。第一个命令我建议不要做得太复杂例如就让终端输出一句问候语commands: hello: description: 测试命令 run: echo hello from cli-anything入口程序的核心逻辑大致长这样import click, yaml, subprocess def load_config(path): with open(path, r, encodingutf-8) as f: return yaml.safe_load(f) click.command() click.argument(command_name) click.argument(args, nargs-1) def main(command_name, args): cfg load_config(commands.yaml) if command_name not in cfg[commands]: click.echo(f未找到命令: {command_name}, errTrue) raise click.Abort() entry cfg[commands][command_name] if run in entry: subprocess.run([entry[run]], shellTrue) # 其他类型的命令在这里扩展 if __name__ __main__: main()这段代码很粗糙但它是一个能跑的最小骨架。把hello配进去之后命令行执行python main.py hello终端会输出hello from cli-anything。到这一步CLI-Anything 的“映射表”机制已经成型了命令名和动作一一对应剩下的就是丰富动作的类型。写完这个骨架之后我立刻把它升级了一下加入了对传入参数的支持。同样是 hello 命令我希望它能接受一个名字参数commands: hello: description: 向你问好 run: echo hello, $name然后在入口程序里把$name替换成实际传入的值。这里最重要的一点是参数传递必须做转义不然一旦传入的值里包含空格或特殊字符命令执行就会出问题。我在这个地方吃过亏后面会专门说。4. 实战扩展把一个网页保存成干净文本骨架跑通之后我做的第一个真正有用的命令是save_page功能是输入一个 URL抓取网页正文清洗掉导航、广告之类的干扰内容再保存成 Markdown 文件。这个任务看起来简单但完整做一遍能覆盖 CLI-Anything 的大多数核心机制。我在配置里把save_page定义成多步骤命令而不是单纯一行 shell。每个步骤都有自己的输入输出前一个步骤处理的结果会成为后一个步骤的输入。这也是 CLI-Anything 比较关键的设计之一——命令不一定只执行单一程序还可以串起一个复杂流程。步骤拆解如下抓取网页 HTML。这一步用的是requests需要设置合理的超时时间还要带上常见的 User-Agent否则不少网站会直接拒绝访问。用 BeautifulSoup 解析 HTML提取标题和正文区域。我默认选择article标签找不到就退回到main或body。把提取到的正文转成 Markdown。这里我用了html2text这个库支持把段落、标题、列表、链接、图片都转成对应的 Markdown 语法。根据网页标题自动生成文件名保存到指定目录。文件名里的特殊字符要清洗掉否则容易在 Windows 或某些文件系统上报错。因为我是用 Click 写的入口命令定义直接声明了一个参数urlclick.argument(url)这样终端输入python main.py save_page https://example.com/article入口程序就会把url传给配置里的执行流程。整个过程大概几秒钟完成比手动打开浏览器、复制正文、整理格式快得多。实际操作中我发现网页正文提取这件事远比想象中麻烦。有些网站正文内容在article里有些在div classpost-content里有些页面还嵌套了评论区、相关阅读这些噪音。我后面加了一个“选择器覆盖”机制允许在配置里指定 CSS 选择器这样不同的网站可以配置不同的提取规则。配置变成这样commands: save_page: description: 把URL内容保存为干净的Markdown文件 args: - name: url required: true steps: - fetch: { url: $url, timeout: 20 } - extract: { selector: $selector, fallback: article } - to_markdown: {} - save: { path: ./output/$title.md } options: selector: article, .post-content, main这个版本支持了“依次尝试多个选择器”的能力。CLI-Anything 的扩展性也是这样一步步长出来的——最开始写死一个选择器后来遇到不同的网站、不同的页面结构逐步把灵活性加到配置层而不是动代码。运行几分钟之后我生成的输出目录里已经有了第一个文件。打开看一眼正文内容完整、标题准确、链接格式正常。那一刻我觉得这个工具的精神已经跑通了——它能把一个原本分散在浏览器、编辑器、手工步骤里的任务压缩成一条命令。5. 踩坑实记与排查思路做到这里CLI-Anything 已经能稳定处理不少任务了。但中间踩的坑一点也不少这里挑几个有代表性的记录一下。这些坑单看都很小但如果不注意足以让整个命令瘫痪。第一个坑是参数转义。最初我图省事直接把参数拼进 shell 字符串里执行结果遇到 URL 里的参数、标题里的空格、中文引号时命令要么被截断要么报错。后来我改成用列表传参的方式完全不经过 shell才彻底解决。这个问题的经验是能不用shellTrue就不要用尽量用subprocess.run([...])直接传参数列表。第二个坑是相对路径。CLI-Anything 如果允许用户在任意目录下执行那么配置里写的./output指向的目录可能完全不一样。我在早期版本里就因为这个生成的文件散落在各个目录完全失控。后面统一成“相对路径以配置文件所在目录为准”之后再也没出过类似问题。第三个坑是超时和重试。抓取网页这个动作很容易卡死尤其遇到响应很慢的网站时默认情况下 HTTP 请求可能挂几分钟。我一开始没设置超时导致整个命令行卡在那里看起来像死机。后来给所有网络请求都加了timeout参数并且针对失败的请求做了一次重试效果好很多。坑现象排查思路解决方法参数转义URL 参数丢失、命令被截断检查传给 shell 的原始字符串用参数列表传参避免 shellTrue相对路径文件输出到意外位置打印执行时的工作目录和路径解析结果统一以配置文件目录为基准解析路径超时命令长时间无响应检查网络请求状态设置合理的 timeout失败后重试中文文件名保存文件时编码错误查看文件系统的编码设置清洗文件名使用安全的字符第四个坑是编码问题。网页抓取中最容易出现乱码尤其是老网站没有声明 charset 的时候。requests 返回的response.encoding有时候是ISO-8859-1直接读文本就会出乱码。我现在的处理方式很简单优先从响应头里拿 charset拿不到就尝试用response.apparent_encoding自动检测。这一步对中文网站的体验提升非常明显。还有一个值得单独说的坑是幂等性。CLI-Anything 的命令最好是可重复执行的也就是说跑两次和跑一次的结果应该一致。有一次我写了一个批量重命名的命令测试时执行成功了再跑一遍的时候因为文件名已经变了行为就和第一次完全不同。后来我在设计命令时养成了一个习惯重要操作执行前先打印将要做的修改并提示确认避免因为误操作造成不可逆的结果。这些坑其实都不是某个框架独有的而是做任何自动化工具都会遇到的问题。把它们记录在这里也是希望你做类似东西的时候可以直接避开。6. 让 CLI-Anything 真正“Anything”起来到这一步CLI-Anything 的基本能力已经完整了。但只停留在“把网页存成 Markdown”显然还不够我会把它继续扩展成覆盖更多场景的通用工具。我接下来的做法是“拆”和“封”。拆是指把大任务拆成小动作单元封是指把每一步动作固化成可复用的命令模块。比如“发一条通知到团队协作工具”这个动作可以被多个上层命令复用“把一张图片压缩到指定宽度”这个动作也可以被很多场景调用。这样 CLI-Anything 就像一个积木箱里面装满了基础积木你可以自由组装成任意工作流。我目前已经在用的几个扩展方向定时触发通过系统自带的定时任务机制定期执行 CLI-Anything 里的命令比如每天早上抓一次新闻页面、每周备份一次配置文件。日志记录所有命令执行都记录日志包括耗时、结果、异常信息这样即使命令半夜执行失败白天也能快速定位。结果通知命令执行完把结果发到手机或团队工具等于给自己的自动化流程装了个“仪表盘”。组合命令定义更高级别的命令内部依次调用多个低级别命令实现流水线式处理。组合命令是我现在用得最多的功能。比如我把“抓取多篇文章→统一转码→生成一个汇总文件”封装成一个命令collect_articles参数是一组 URL。这个命令本身没有新增任何技术难度只是把已有的模块串起来但产出的价值一下子高了很多。这就是 CLI-Anything 最吸引我的地方——它的复杂度是线性增长的每加一个基础命令就等于新增一种组合可能性而组合带来的收益往往远大于单个命令。如果你也想搭一套自己的 CLI-Anything我的建议是从一个真实的小任务开始而不是从框架开始。写一个能用的命令哪怕只是“把当前目录下所有图片压缩一下”也比设计一个完美的抽象体系有价值得多。因为只有真实任务才能逼你面对路径、编码、超时、重试这些细节问题而这些细节恰恰是决定工具好不好用的关键。我个人在实际操作中还有一个体会给命令写说明注释非常值得。每当我在配置文件里加一个新命令我都会顺带写上一段“这个命令解决什么问题、参数是什么意思、输出在哪里”。三个月后回头看这些注释节省的回忆时间远超当初写它们的时间。CLI-Anything 这个名字听起来很大但真正让它跑起来、用顺手的往往都是这些不起眼的小习惯。