ARTICLE DETAIL

资讯详情

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

用YAML声明式配置:把重复劳动变成命令行命令

用YAML声明式配置:把重复劳动变成命令行命令 接手过几十个项目之后我越来越相信一件事多数重复劳动的尽头都是命令行。不是因为你非要装成一个整天敲终端的极客而是因为凡是需要反复做两遍以上的操作都值得被自动化。CLI-Anything 就是我从这个念头里长出来的一个项目——它把一个很朴素的想法变成了现实任何东西只要你能描述清楚就能变成一条命令行命令。CLI-Anything 不会让你去重新学一门编程语言它只负责搭一个壳子你在 YAML 里写好这个命令叫什么、需要哪些参数、内部调用什么脚本或 APICLI-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.yamlname: 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 提供了两个扩展 hookbefore_step和after_step。在配置中可以这样做executor: type: python script: process.py运行时 CLI-Anything 会把当前步骤涉及的所有参数和环境变量写入一个 json 文件然后调用脚本读取这个 json业务逻辑做完后把结果写回一个 jsonCLI-Anything 再把结果注入到下一个步骤。这类似中间件模式但剥离了复杂的消息总线。对团队来讲最大的好处是每个人可以用自己熟练的语言写执行器不需要从零去学一套 SDK。自定义执行器如果命名约定以-cli-anything-executor开头CLI-Anything 会自动通过 entry point 发现它加载成新的type值。这个设计参考了 Python 插件生态其实实现起来并不复杂关键是定义好输入输出协议。你可以在项目里写一个几十行的模块就能给 CLI-Anything 增加一种没人见过的执行方式这种扩展体验几乎是最爽的。4.2 并发执行与超时控制如果你要批量跑几十个任务一个个执行太慢。可以让命令定义支持并发设置run: concurrency: 4 timeout: 30CLI-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指定一个简单的 KEYVALUE 文件运行时会自动加载且不会把内容回显到日志这比手动复制.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 流水线因为不是 TTYprompt_toolkit会直接报错或者无限等待用户输入。CLI-Anything 给了两个开关全局的CLI_ANYTHING_NON_INTERACTIVE1环境变量以及每个命令定义上的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 或 enum3缺少必填环境变量用env_file或在 shell 中 export4命令定义文件不存在确认.cli/目录与CLI_ANYTHING_PATH5交互模式下检测到非 TTY设置CLI_ANYTHING_NON_INTERACTIVE16步骤执行超时调大run.timeout或优化步骤本身这张表是我自己维护的不一定完全贴合其他版本但排查顺序每次都差不多。先把退出码定位到问题层面再往里钻具体细节会比四处乱试快得多。我用 CLI-Anything 接手的第一个真实项目是用它把散落在各个文档里的部署指令变成了一条deploy命令。那次经历让我明白工具的核心价值不在于代码本身多惊艳而是它愿意为你重复的工作建立一个统一的入口。现在我会定期翻看自己的.cli/目录把频繁操作的命令都放进去把慢慢不再需要的删掉它实际上已经变成了我的个人工作手册。如果你也有那种每周都要手动做一遍的流程找个周末把它写成一份 YAML你会发现命令行离普通工作其实没那么远。
返回列表