☰
用YAML声明式配置:把重复劳动变成命令行命令
2026/9/28 16:30:43 网站建设 项目流程

接手过几十个项目之后,我越来越相信一件事:多数重复劳动的尽头,都是命令行。不是因为你非要装成一个整天敲终端的极客,而是因为凡是需要反复做两遍以上的操作,都值得被自动化。CLI-Anything 就是我从这个念头里长出来的一个项目——它把一个很朴素的想法变成了现实:任何东西,只要你能描述清楚,就能变成一条命令行命令。

CLI-Anything 不会让你去重新学一门编程语言,它只负责搭一个壳子:你在 YAML 里写好这个命令叫什么、需要哪些参数、内部调用什么脚本或 API,CLI-Anything 就会自动帮你生成一个合规的 CLI 入口、帮助信息和参数校验。开发、测试、运维、数据分析,甚至一些产品同学,只要愿意写几行配置,就能把自己手里那摊重复操作变成团队共享的命令。这篇文章我会把它的设计思路、核心实现、三个实际封装场景和常见坑一次讲透,你可以直接照着抄。

1. 为什么需要CLI-Anything:从真实痛点说起

1.1 命令行不是万能,但大部分工作流都能统一

我日常工作里有大量需要跨窗口完成的任务。举个最简单的例子:给一组图片做压缩。你可能会先打开截图软件导出,再拖进压缩工具,再上传到服务器,每一步都在不同的窗口里来回切换。如果只是偶尔一张,忍忍就过去了;可如果是一次性处理几百张图,手动操作就是慢性自杀。命令行天然适合这种批量、结构化、可重复的任务,问题在于,很多人不是不会写命令,而是不想为了每个新任务都去写一堆一次性脚本,更不想研究每种工具的 CLI 语法都存在什么差异。

CLI-Anything 抓住的正是这个缝隙:与其让你去学习每个工具的 CLI 参数,不如让你用一份统一的声明文件,把现有命令组合成一条新命令,并且像面对专业 CLI 工具一样使用它。它不试图取代任何底层工具,它只做一个映射层。这个定位很重要,因为一旦你决定自己封装一堆脚本,马上就会遇到"脚本越来越多、越来越乱,半年后自己也看不懂"的困境。

1.2 我理想中的CLI封装层应该长什么样

在动手写 CLI-Anything 之前,我先列了一个需求清单,后来它直接变成了设计原则。第一,声明式配置优先,不想为了一个简单操作去写完整代码,用 YAML 就能定义命令。第二,即装即用,安装之后不需要额外做初始化。第三,自动生成帮助和补全,这是专业 CLI 的基本素养。第四,可插拔,默认支持执行 shell 步骤,也允许接入 Python、Node 等脚本。第五,不绑架运行环境,执行器既能直接调用系统命令,也可以调用 Docker 容器里的工具。

这些需求背后有一个共同逻辑:把 CLI 外的复杂度隔离在配置层之外,让使用者只面对命令和参数。可能有人觉得这只是一个 shell 脚本的封装器,但对我来说,它是一层统一的工作协议。团队里不管谁写的操作流程,只要按这个协议描述出来,大家就能用同一种方式调用,这才是真正能沉淀下来的东西。

2. 核心设计拆解:做一个通用命令行映射框架

2.1 配置即命令:用一份YAML描述所有流程

CLI-Anything 的核心概念是"命令定义文件"。一个文件对应一条命令,文件名就是命令名。比如你创建一个hello.yaml,里面写着:

name: hello description: 说一句你好 args: - name: who prompt: 想对谁说 required: false default: world steps: - echo "hello ${who}"

当你运行cli-anything hello --who cli时,它会先读取配置文件,做参数校验,然后把变量注入到steps里逐条执行。这段配置背后的处理流程,其实就是在模拟一个简单 shell 脚本的生成和解释过程,但它不需要你写脚本,也不需要你自己处理复杂的引号和转义。为了让变量传递更安全,CLI-Anything 会把每个参数值通过 base64 编码传给执行层,执行的时候再解码,这样能避免很多莫名其妙的空格问题。

另外,CLI-Anything 不需要一个集中的配置目录。默认情况下会从当前目录的.cli/文件夹读取,也能通过环境变量CLI_ANYTHING_PATH指定多个路径。这个设计借鉴了 PATH 环境变量的思路:命令定义文件分散在项目或用户目录,但只要你把它们放到搜索路径里,就可以被全局访问。我自己在~/.cli/里放了一批通用命令,在项目.cli/里放跟业务相关的命令,两边互不干扰。

2.2 参数解析与交互提示的折中

CLI 工具如果只有参数解析,很多场景还是不够友好。比如你临时想执行一个命令,但记不住必填参数,这时如果能交互式问你一句,体验会好很多。CLI-Anything 因此实现了两种输入方式并存:用户在命令行给了参数,就直接使用;没有给且配置里标记了prompt,就进入逐项询问。实现上它没有自己写解析器,而是构造prompt_toolkit的询问会话,把每个args配置项循环一遍,所以天然支持 Tab 补全和历史输入。

参数校验分三个层级:required标记、regex正则、enum枚举可选值。如果配置了enum又传入了非法值,CLI-Anything 会直接列出可选列表,非交互模式下会以非零状态码退出。可选参数default的处理也有讲究:如果配置了默认值而用户没传,它不会把空值传给步骤,而是把默认值显式注入进去,这样在步骤里做条件判断或拼接字符串时,就不会遇到空引用陷阱。

2.3 自动补全与帮助文档生成

很多人会低估自动补全的价值。CLI-Anything 会在第一次运行时检测当前 shell,并把一个补全脚本写入对应配置目录。比如 zsh 用户运行一次后,就会有一个_cli-anything的文件被加载进fpath,之后敲cli-anything命令名,再按 Tab,就会列出所有可用的命令文件;继续按 Tab,当前命令的参数名和可选项也会出现。这个能力并不需要用户为每条命令手动写补全逻辑,因为命令名来自文件名,参数名来自args配置,选项来自enum枚举,所有信息都在声明文件里,补全脚本就是基于这些元数据自动生成的。

同时,它还会生成一个completions.json,供一些编辑器插件消费。帮助文档也是同理,无论运行cli-anything help xxx还是cli-anything xxx --help,都会根据description、args、steps这些字段渲染出一份格式统一的手册,里面包含命令名、参数说明、示例和退出码定义。这一点在团队协作里特别加分,新成员即使不看 README,直接--help也能知道这个命令是干嘛的、怎么用。

3. 实操过程与核心环节实现

3.1 场景一:把图片批量压缩命令化

先看一个实际场景。服务器上经常需要把用户上传的图片压缩成指定宽度。手工步骤是:遍历文件、调整大小、覆盖保存。如果用 CLI-Anything,我在项目的.cli/下创建一个thumb.yaml:

name: thumb description: 批量生成缩略图 args: - name: source prompt: 图片所在目录 required: true - name: width default: 800 validate: "^[0-9]+$" steps: - mkdir -p ${source}/thumbs - for img in ${source}/*.{jpg,jpeg,png}; do convert "$img" -resize ${width}x "${source}/thumbs/$(basename "$img")"; done

这段步骤本质上是一段 shell,但 CLI-Anything 帮你把参数解析和变量拼装做了。运行cli-anything thumb --source /tmp/photos --width 640,就会自动生成缩略图目录并控制尺寸。我用 1000 张图测试过,没有碰到脚本环境下引号解析的问题,这得益于运行前会对所有用户参数做一次正则校验。如果你用的是 macOS,注意convert可能是另一个软件,可以换成系统自带的sips工具。

这种做法的价值在于,你不需要在每次需要时重新想这段逻辑;而且团队里其他人也能直接用。只要执行cli-anything thumb --help,就能知道怎么调用,不需要我把命令贴在文档里还要担心文档过时。

3.2 场景二:把REST API调用链封装成交互命令

另一个我几乎天天在用的场景是调试后端接口。以前要么打开 Postman,要么临时写 curl,环境不一样时 URL 前缀和 Token 也各不相同。现在我把整个调用过程配置成一个命令:

name: api-call description: 调用订单查询接口 env: - BASE_URL - API_TOKEN args: - name: order_id prompt: 输入订单号 required: true validate: "^ORD[0-9]{8}$" steps: - curl -s -H "Authorization: Bearer ${API_TOKEN}" ${BASE_URL}/orders/${order_id}

配置里的env字段是一个很关键的设计:它声明了这个命令依赖哪些环境变量,运行前 CLI-Anything 会检查,缺失就提示用户先设置好,避免到步骤执行时才报错。这种"环境前置检查"的思路,让 CLI 命令变得健壮很多。

如果是多步调用,比如先创建订单、再查询状态,可以在steps里写一行节点脚本,或者用&&连接多段命令。我自己的做法是让步骤输出 JSON 到一个临时文件,下一步用python -c读取,实现数据流的传递。你不需要为此额外装一套工作流框架,只要 CLI-Anything 能在步骤之间共享临时文件,它就是你的轻量级流程编排器。比起一开始就引入重型工具,这种方式轻太多,也更容易被团队接受。

3.3 场景三:把Git工作流封装成带安全检查的命令

团队协作中的 Git 操作是出错率很高的场景。我见过太多人因为忘了切换分支、忘了跑测试,直接把半成品推到远程。CLI-Anything 很适合把标准流程固化成命令:

name: ship description: 合并到主干并部署 args: - name: target_branch default: main enum: ["main", "release"] steps: - git fetch --prune - current_branch=$(git branch --show-current) - if [ "$current_branch" = "${target_branch}" ]; then echo "不能直接在目标分支提交" && exit 1; fi - git add -A - git commit -m "chore: release from ${current_branch}" - git checkout ${target_branch} - git pull --ff-only - git merge ${current_branch} --no-ff -m "merge ${current_branch}" - git push origin ${target_branch}

通过这样一条命令,原本必须记住的八步操作变成了一次调用,而且里面加上了自我保护逻辑:当前分支等于目标分支时直接退出。这条命令我不会在敏感项目里让所有人无脑使用,但它非常适合做个人自动化习惯。大家真正应该学到的思路是:把频繁操作标准化之后,人为出错的概率会下降一个数量级,因为你只需要检查一次脚本写没写对,后面每次执行都是在重复验证过的过程,而不是每次都临时临场发挥。

4. 进阶:扩展机制与运行细节

4.1 自定义插件:给CLI-Anything加一个执行器

前面几个例子都是直接执行 shell 步骤,但真实项目中总会遇到需要传递复杂数据结构的情况,这时用 shell 处理稍微有点勉强。CLI-Anything 提供了两个扩展 hook:before_step和after_step。在配置中可以这样做:

executor: type: python script: process.py

运行时 CLI-Anything 会把当前步骤涉及的所有参数和环境变量写入一个 json 文件,然后调用脚本读取这个 json,业务逻辑做完后,把结果写回一个 json,CLI-Anything 再把结果注入到下一个步骤。这类似中间件模式,但剥离了复杂的消息总线。对团队来讲,最大的好处是每个人可以用自己熟练的语言写执行器,不需要从零去学一套 SDK。

自定义执行器如果命名约定以-cli-anything-executor开头,CLI-Anything 会自动通过 entry point 发现它,加载成新的type值。这个设计参考了 Python 插件生态,其实实现起来并不复杂,关键是定义好输入输出协议。你可以在项目里写一个几十行的模块,就能给 CLI-Anything 增加一种没人见过的执行方式,这种扩展体验几乎是最爽的。

4.2 并发执行与超时控制

如果你要批量跑几十个任务,一个个执行太慢。可以让命令定义支持并发设置:

run: concurrency: 4 timeout: 30

CLI-Anything 收到这个选项后,会把steps数组中的同级步骤并行执行,同时用 context 超时机制控制每个步骤的最长运行时间,超时的进程会被强制杀掉并记录状态。注意这里的"同级"指的是数组中的元素,如果是用&&连接的复合 shell 命令,会被当成一个整体,不会拆开并发。

我踩过的坑是并发数开太高,比如同时启动二十个 subprocess,机器 CPU 直接打满,最后反而更慢。实际经验是,文件操作类任务并发数等于 CPU 核心数再加一比较合适;网络 I/O 类任务可以适当放宽,但也不要超过十,否则很容易打爆服务器的文件描述符。组合步骤时,错误传播策略也很重要。默认只要某个步骤失败,CLI-Anything 就停止执行并返回非零退出码,这也是推荐行为。如果你希望某个非关键步骤失败不影响结果,可以给它加continue_on_error: true,这样就不会中断整条命令。

4.3 跨平台打包与移植经验

CLI-Anything 本体是用 Go 写的,好处是最终只是一个二进制文件,扔到任何服务器上都能直接跑,不依赖本机 Python 或 Node。但跨平台也带来一些麻烦。最常见的是路径分隔符差异:Windows 是反斜杠,Linux 和 macOS 是正斜杠。CLI-Anything 在解析配置文件里的路径时做了一件事:把所有${}变量的值统一替换成平台原生分隔符,并且在拼接 shell 步骤时,如果检测到当前系统是 Windows,优先使用powershell -Command而不是cmd /c,因为 cmd 对引号的处理实在反人类。

如果你在 Windows 上跑 Linux 风格的命令,推荐直接配置好 Windows Subsystem for Linux,让 CLI-Anything 通过wsl exec来调用,这样 shell 语法完全不用改。还有一个细节是环境变量在 Windows 下大小写不敏感,但 CLI-Anything 内部总是把env字段转换成大写再比较,不管用户写的是api_token还是API_TOKEN,都不会踩坑。

跨平台测试没有捷径。我写了一个简单的 GitHub Actions 矩阵,在 ubuntu、macos、windows 三个系统上跑同一个demo.yml用例,每次提交都能看到输出是否一致。这个习惯值得每一个做 CLI 工具的人养成,因为"在我的机器上是好的"这句话,在团队里真的不成立。

5. 常见问题与排查技巧实录

5.1 命令找不到或环境变量地狱

我收到最多的反馈是"我配好了,但提示 command not found"。排查思路其实很简单:先运行which cli-anything看二进制是否在 PATH 里;再运行cli-anything list看能不能识别到配置文件;如果 list 为空,检查当前目录和CLI_ANYTHING_PATH的设置。一个很隐蔽的问题是配置文件带了扩展名,比如ordered.yaml,默认建议用.yaml,但如果你在其他平台复制文件时变成了大写.YAML,在某些大小写敏感的系统上就会直接找不到。

环境变量柜也是重灾区。如果只在 shell session 里 export,下次开新终端就丢了。CLI-Anything 支持在配置中通过env_file指定一个简单的 KEY=VALUE 文件,运行时会自动加载,且不会把内容回显到日志,这比手动复制.env方便很多。前提是你要非常注意权限,这个文件最好设置成 600,否则等于把密钥明晃晃写在命令行里。

5.2 参数引号与转义问题

把用户提供的参数拼进 shell 命令,引号问题最折磨人。比如你想运行rm "${source}/$(basename "$img")",在 CLI-Anything 内嵌的 shell 步骤里,因为参数值会被替换成明文,一旦source里包含空格或单引号,就可能出错。我当时的应对策略是:CLI-Anything 为每个参数提供了一种as_array选项,开启后不再做字符串替换,而是把参数值以 JSON 数组形式传入,步骤中可以用数组展开的方式安全使用。

另外,如果步骤比较复杂,可以完全不用 shell 语法,而是让 CLI-Anything 调用一个外置脚本,脚本从参数文件里读取数据,这样任何特殊字符都不会进 shell。经验法则:凡是步骤超过 30 行,就别嵌在 YAML 里了,直接写个独立脚本,让 YAML 去调用它更靠谱。你可能会想保留一段能在命令行直接粘贴的步骤,但维护起来真的很痛苦。

5.3 交互模式在CI中失效的坑

交互式提示在真实终端里很好用,但一旦进入 CI 流水线,因为不是 TTY,prompt_toolkit会直接报错,或者无限等待用户输入。CLI-Anything 给了两个开关:全局的CLI_ANYTHING_NON_INTERACTIVE=1环境变量,以及每个命令定义上的interactive: false。CI 脚本里推荐显式传入所有必填参数,并加上--yes跳过所有的警告确认。

我见过有人在 GitHub Actions 里跑一条本来有交互确认的命令,结果 job 挂了几十分钟直到超时。解决办法是:如果你在自己的命令步骤里写了read -p "确认?",记得在外层加一个if [ ! -t 0 ]; then echo "非交互模式,跳过"; exit 0; fi判断。CLI-Anything 的很多稳定使用习惯,都是在 CI 的失败中被迫练出来的,所以如果你要跑自动化,一定要在写命令时就考虑非 TTY 场景。

5.4 退出码速查表

CLI-Anything 的退出码约定比较固定,排查问题时可以按表格对照。

退出码含义常见排查动作
0执行成功无需处理
1步骤中的命令返回了错误手动运行该步骤命令,观察输出
2参数校验失败检查传入参数是否满足 regex 或 enum
3缺少必填环境变量用env_file或在 shell 中 export
4命令定义文件不存在确认.cli/目录与CLI_ANYTHING_PATH
5交互模式下检测到非 TTY设置CLI_ANYTHING_NON_INTERACTIVE=1
6步骤执行超时调大run.timeout或优化步骤本身

这张表是我自己维护的,不一定完全贴合其他版本,但排查顺序每次都差不多。先把退出码定位到问题层面,再往里钻具体细节,会比四处乱试快得多。

我用 CLI-Anything 接手的第一个真实项目,是用它把散落在各个文档里的部署指令变成了一条deploy命令。那次经历让我明白,工具的核心价值不在于代码本身多惊艳,而是它愿意为你重复的工作建立一个统一的入口。现在我会定期翻看自己的.cli/目录,把频繁操作的命令都放进去,把慢慢不再需要的删掉,它实际上已经变成了我的个人工作手册。如果你也有那种"每周都要手动做一遍"的流程,找个周末把它写成一份 YAML,你会发现命令行离普通工作其实没那么远。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询