尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

CLI工具开发实践:用YAML配置统一封装内部API与脚本

发布时间:2026/9/29 19:44:41

资讯中心
01
ARTICLE

CLI工具开发实践:用YAML配置统一封装内部API与脚本

CLI工具开发实践:用YAML配置统一封装内部API与脚本
写内部工具写了七八年我发现自己卡在同一个问题上入口太多了。订单接口在 Swagger 页面里部署逻辑散落在三个脚本中服务器状态要看另一个内部系统的网页每次想对数据做点批量操作还得临时手写 curl。时间一长找入口这件事本身就变成了一种隐性成本。所以我自己折腾了一个叫 CLI-Anything 的小框架思路很简单只要写一份声明式 YAML 配置就能把任意 HTTP 接口、本地脚本、甚至一串串联操作统一封装成本机可用的 CLI 命令。如果你手里也有一堆零散的内部 API 和运维脚本想把操作收敛到终端里这篇东西应该能给你一个可以直接抄作业的参考。很多人第一反应是直接写 Shell 脚本不就行了吗还真不行。脚本一旦变多参数的校验、错误信息、输出格式、补全逻辑都要自己从零维护写到最后往往比接口本身还复杂。CLI-Anything 本质上是一个“命令工厂”配置决定命令长什么样适配器决定命令背后调什么。用户不需要关心实现细节只要记住了命令名和参数剩下的交给框架。1. 需求从哪里来散落入口的痛感1.1 分散入口带来的真实成本我用过的内部系统几乎都有一个共同特征功能确实有但入口分布在不同的地方。比如订单系统有一个查询接口你能在浏览器的调试工具里看到请求但每次想手动查一条订单得先打开页面、登录、找到调试面板再把参数复制进去。如果想把查询结果接进自动化流程还得自己去处理鉴权、超时、重试这些事。另一个更隐蔽的成本是“上下文切换”。当你正在终端里处理日志突然要切到浏览器里去查一个用户信息再切回来继续处理这个过程看起来只有几十秒但实际上打乱了你在命令行里的工作流。CLI-Anything 想解决的就是这个问题把一切能暴露成接口的东西统一收编到命令行里。可用下面的方式调用那么接口有还是没有、在哪个系统里都不再重要。“统一收编”也是我选择这个方案的核心原因。团队里不同的人维护不同系统如果你的查询命令能像git一样通过子命令切换那协作成本会大大降低。新人不需要知道订单服务的内网地址是什么也不需要知道部署脚本传什么参数所有高电平细节都藏在一个命令名后面。1.2 为什么选声明式配置而不是继续堆脚本早期我用的办法很简单每个工具建一个目录里面放一个 Python 或者 Shell 脚本。用的时候调用脚本参数靠argparse或者$1 $2。这种方式在只有三五个工具的时候很舒服一旦工具到二十个以上你会发现几个问题每个脚本都要自己处理参数校验经常是同样的逻辑复制粘贴。脚本之间的“长相”完全不统一有的用--order-id有的用-i有的默认必填有的默认可选。测试和文档都缺位。时间久了你连自己写的脚本怎么用都记不住。声明式配置的好处是把“命令形状”和“实现逻辑”分开。命令有哪些参数、什么类型、调什么接口、传什么 header这些全写进 YAML。框架统一负责参数解析、校验、帮助信息生成实现方只需要暴露一个简单的适配器。这样一份配置就是一个命令新增命令不需要写业务逻辑只需要写 YAML。有人会问这跟现成的 OpenAPI 生成工具有什么区别CLI-Anything 更激进一点它不只接 HTTP API也能接本地脚本、远程函数甚至一组命令的组合。它更像是“胶水层”把不同的执行目标用同一种交互方式暴露出来。2. 核心机制拆解一条命令是怎么跑起来的2.1 从敲下命令到拿到结果中间发生了什么先看一个最简单的流程。假设配置里定义了一个query-order命令你在终端敲cli query-order --order-id 20240315001CLI-Anything 接下去做这几件事解析命令名查找配置里对应的commands.query-order节点。根据该节点定义的params列表解析并校验参数类型。根据adapter类型路由到对应的适配器实现。适配器把参数和配置合并成一次真实的调用比如 HTTP 请求、子进程、函数调用。拿到调用结果后按照配置里的output格式输出到终端。这个流程听起来不复杂但关键其实在第二步和第四步。第二步决定了命令对用户是否友好第四步决定了它能接多少种“任意的东西”。我把参数模型设计成三件套name、type、required。type支持string、int、float、bool、choice、flag其中flag有点特殊它不取后面的值只是布尔开关。这样设计的好处是适配器拿到参数时已经是一个类型安全、已经过校验的数据结构不需要自己做二次解析。2.2 参数解析说得清楚才能校验得准一个命令好不好用参数解析占了大半。CLI-Anything 内部用 Typer 作为参数解析引擎但在配置层我做了额外的一层抽象。理由很简单Typer 的装饰器写法适合代码内定义命令但我要的是“配置驱动”命令长什么样不该写死在代码里。一个典型的参数定义长这样params: - name: order_id type: string required: true help: 订单编号 - name: verbose type: flag default: false help: 是否打印详细请求信息你会发现定义里没有位置信息。CLI-Anything 默认把所有参数都生成--xxx形式这有几个好处避免位置参数顺序记错。可选参数天然有默认值。--help输出一目了然。这算是我私心的一点坚持内部工具的参数永远比 CLI 本身的代码重要。因为用命令的人是队友他们没时间看你的源码。2.3 适配器体系让“Anything”真正成立参数解析完接下来就是执行。我把执行方式抽象成一个接口每个适配器只需要实现一个简单的调用逻辑class BaseAdapter: def run(self, ctx: CommandContext) - AdapterResult: raise NotImplementedErrorCommandContext包含配置、解析后的参数、环境变量、输入流。AdapterResult统一返回标准输出、错误信息、退出码。目前我内置了三个最常用的适配器http处理 HTTP 请求。URL 支持参数插值headers 支持读取环境变量。shell执行本地命令。适合把原有的 Shell 脚本包成 CLI。chain串联多个子命令。适合定义发布、测试、部署这类复合流程。这个接口最大的价值是扩展新适配器不需要改命令管理逻辑。比如某个系统只有 Python SDK不提供 HTTP API那我可以写一个python适配器在适配器内部 import SDK把参数映射到 SDK 函数。新增适配器的成本大概只有几十行代码而命令定义完全不用动。2.4 输出、调试与格式约定CLI 工具最容易忽略的是输出格式。很多脚本喜欢读个字节返回一段看起来能跑但无法嵌入自动化。CLI-Anything 默认输出 JSON因为 JSON 天生结构化能被jq或者其他命令消费。但二进制输出怎么办比如你要用 CLI 下载一个文件到本地。解决办法是在配置里指定raw_output: true这种情况下适配器把返回值透传到标准输出CLI 不做任何格式化。为了调试方便每个命令我都加了两个隐藏参数--dry-run只打印将要执行的请求或者命令不真正执行。--debug打印适配器接收到的完整参数和返回的原始内容。这两个参数对我排查问题帮助极大。很多时候用户觉得“命令坏了”其实只是参数没传对或者返回值被格式化吞掉了。有了--debug问题基本一眼能看出来。3. 实操把三件完全不同的事变成 CLI3.1 安装和初始化CLI-Anything 的安装很简单它是一个 Python 包pip install cli-anything初始化方式也走配置驱动你只需要建一个目录放一个cli.yaml。比如我自己的习惯~/.cli/ cli.yaml adapters/ custom_python.py logs/cli.yaml是总配置adapters/放自定义适配器。启动命令cli --config ~/.cli/cli.yaml或者把它设成别名alias ccli --config ~/.cli/cli.yaml配置可以直接放在家目录下也可以放在项目的.cli/目录里。我的建议是如果一个命令只有你自己用就放家目录如果团队要用就放进代码仓库跟着项目走。3.2 场景一把 HTTP 接口包装成查询命令假设有一个订单查询接口GET https://api.internal.example.com/orders/{order_id} Header: Authorization: Bearer token在 CLI-Anything 里只要写这么一段commands: query-order: description: 查询订单状态 adapter: http method: GET url: https://api.internal.example.com/orders/{order_id} headers: Authorization: Bearer ${env.API_TOKEN} params: - name: order_id type: string required: true help: 订单编号 output: json执行效果c query-order --order-id 20240315001输出{order_id: 20240315001, status: shipped, updated_at: 2024-03-15T10:23:00Z}这里面有两点值得展开URL 里的{order_id}是变量插值CLI-Anything 会把同名参数传进去。如果参数值里面有特殊字符框架会做 URL 编码防止请求 URL 被破坏。headers 里的${env.API_TOKEN}表示从环境变量读取这避免了把 token 写死在配置文件里。配置文件可以提交到 Git 仓库但 token 永远留在环境变量中。我在实际使用中还发现HTTP 接口经常会有默认值、分页参数这些你需要保持一致。CLI-Anything 会把所有已配置的字段以参数形式暴露出来所以即使接口有十个字段你也不需要全记只需要在 YAML 里定义你常用的那几个其余字段不定义就不能传这从源头减少了误操作风险。3.3 场景二把部署脚本包成 CLI很多团队都有部署脚本但脚本参数名不统一、校验也弱。CLI-Anything 可以把它们全部收拾整齐。假设有个deploy.sh用法是./deploy.sh --env prod --tag v1.2.3配置如下commands: deploy: description: 部署到指定环境 adapter: shell cmd: ./scripts/deploy.sh --env {{env}} --tag {{tag}} params: - name: env type: choice choices: [dev, staging, prod] required: true help: 目标环境 - name: tag type: string required: true help: 镜像版本号 env: KUBECONFIG: {{config.kubeconfig}} timeout: 120几点说明cmd模板里的{{env}}和{{tag}}来自命令参数。我在这里刻意避免使用{env}这种写法是为了和 Shell 变量在视觉上区分开。它们本质上都是模板插值但命名方式让人一眼能看出谁是谁。env字段可以给子进程注入额外的环境变量。这里{{config.kubeconfig}}会读取cli.yaml里的全局配置项适合放像路径、地域这类共享信息。timeout: 120是超时时间。Shell 脚本经常会有长时间任务默认超时 60 秒容易误杀所以我把超时做成显式配置。这种方式的价值是你的部署流程无论埋了多少层最后给人的感觉都像一条简单的命令。新人不用知道deploy.sh里做过哪些迁移、有哪些前置检查他们只需要知道c deploy --env prod --tag v1.2.3。3.4 场景三用一个命令完成“测试构建发布”大多数发布流程不是单个动作而是一连串步骤。CLI-Anything 的chain适配器专为此设计commands: release: description: 执行完整发布流程 adapter: chain params: - name: env type: choice choices: [staging, prod] default: staging help: 发布环境 - name: tag type: string required: true help: 镜像版本号 steps: - command: test - command: build --tag {{tag}} - command: deploy --env {{env}} --tag {{tag}}steps里的每一项会被依次执行前一条命令的退出码不为 0 时后续步骤自动中止。这样你可以把“发布”从一句口头禅变成一条可审计、可复现的命令。chain还支持把上一步的结果传递到下一步。在配置里用{step_name.output}引用即可steps: - name: build_version command: get-version - command: deploy --tag {{build_version.output.version}}这种“命令管道”的能力让我感觉像在终端里拼乐高。本来需要手工三步操作的工作封装成一个命令之后出错的概率大幅下降。3.5 自动补全让命令像自带说明书CLI-Anything 支持为命令生成 Shell 补全脚本c completion bash ~/.cli/completion.bash然后在~/.bashrc或~/.zshrc里加一行source ~/.cli/completion.bash之后你在终端里按两次 Tab就能看见所有命令名、命令参数、参数枚举值。这个功能对“让队友爱上命令行”有奇效。人的记忆是有限的与其靠背诵参数不如让补全把参数列表直接弹出来。4. 常见问题与排查实录4.1 参数被 Shell 吃掉命令变成“看起来没错但结果不对”这个坑我踩得最多。比如你写c query-order --order-id 20240315001看起来正常但如果配置里order_id是必填CLI-Anything 的解析引擎只能拿到字符串。真正的问题往往发生在值里有空格或特殊字符时。比如 tag 是v1.2.3-beta18在 Shell 里要记得加引号c deploy --env prod --tag v1.2.3-beta18如果发现命令单独执行没问题放到 CI 或脚本里就报参数错误十有八九是 Shell 展开了特殊字符。排查思路很简单先跑c deploy --dry-run看框架实际收到的参数是什么如果收到的参数和你想传的不一样那就是 Shell 引用问题。CLI-Anything 内部对传入参数只做字符串接收不会自作聪明地去反转义。为了保证安全性框架在把参数传给 Shell 适配器时默认参数值不会经过命令行解释而是通过环境变量传递。配置里可以用{{env}}这样的写在cmd里的位置参数但在我的实现里普通参数值会先写入子进程环境变量再由cmd的值引用。这能避免注入风险不过也意味着你在写 Shell 命令时要清楚哪些环节会受 Shell 语法影响。4.2 中文输出乱码内部系统返回的 JSON 经常带中文。如果你直接用print(result)输出大概率会遇到 GBK 编码错误。CLI-Anything 的解决办法是所有网络请求和文件读取都强制utf-8输出时也强制utf-8。如果你发现自己起了一个自定义适配器内部用了subprocess调用外部命令要注意外部命令的输出编码。Windows 下很多命令输出的是 GBK我在适配器接收子进程输出时先尝试utf-8失败则回退gbk。这个“先 utf-8 后 gbk”的回退策略帮我解决了大量诡异的乱码问题。说实话这种问题在 macOS 和 Linux 上很少出现但只要有人用 Windows早晚会遇到。4.3 网络接口超时和重试调用内部 API 时最常见的问题是服务偶尔变慢。CLI-Anything 默认 HTTP 超时是 10 秒你可以全局调整global: http_timeout: 30 retry_times: 3 retry_delay: 1retry_delay指的是每次重试之间的等待秒数。这里我建议设置一个稍微保守的值不要 0 延迟重试否则接口一抖动重试反而把服务打得更差。另外内网系统很多时候要走代理但代理偶尔会拖慢请求。我在适配器里支持no_proxy配置确保访问内网地址时不走代理。这个细节在排查的时候尤其重要我曾经花了一个下午找架空的问题最后发现是代理把内网请求劫持了。4.4 Windows 下的路径兼容问题CLI-Anything 为了跨平台Shell 适配器执行子进程时不走shellTrue。在 Windows 上这意味着.sh脚本不能直接执行需要显式调用bash。一个比较省事的配置是cmd: bash ./scripts/deploy.sh --env {{env}}如果你又用了变量插值要注意路径分隔符。比如KUBECONFIG在 Windows 下如果传了一个C:\xxx\kubeconfig反斜杠会被当成转义符。我在模板插值函数里默认把反斜杠转成双反斜杠避免这类问题。如果你打算在 Windows 上跑尽量习惯用pathlib或者正斜杠写路径别和反斜杠较劲。下面是我整理的一份常见问题速查表症状可能原因排查方式命令提示参数缺失Shell 展开了特殊字符或空格用--dry-run看实际参数中文输出乱码外部命令输出非 utf-8开启调试检查原始返回请求超时代理劫持或服务抖动配置no_proxy调大超时命令返回值很奇怪环境变量未传递检查配置里的env段Windows 下命令找不到脚本没有显式调用 bash把cmd改成bash xxx.sh5. 从“能用”到“顺手”我的扩展心得5.1 自定义适配器该怎么做内置适配器覆盖了 80% 的场景剩下 20% 往往需要对接内部 SDK。CLI-Anything 允许你在adapters/目录放一个 Python 文件from cli_anything import BaseAdapter, CommandContext, AdapterResult class PythonSDKAdapter(BaseAdapter): name python_sdk def run(self, ctx: CommandContext): client some_sdk.Client(tokenctx.env[SDK_TOKEN]) result client.query(ctx.params[user_id]) return AdapterResult(okTrue, dataresult)然后在配置里这样引用commands: user-info: adapter: python_sdk params: - name: user_id type: string required: true这个适配器最大的好处是你不需要把 SDK 的逻辑暴露成 HTTP 服务也不需要写成一堆脚本直接在 CLI 层完成封装。对于内部系统来说这是成本最低的集成方式。5.2 把 CLI-Anything 当成团队命令手册我后来形成了一个习惯把cli.yaml提交到一个公共仓库里所有人 clone 下来跑一下cli --init就能开始用。这份配置就是团队的命令手册新增接口不用发文档直接 PR 配置文件就行。为了让这个“手册”更好用我给命令都写了description和每个参数的help。CLI-Anything 生成--help时会把它们拼进去效果等同于内置文档。这样队友不需要翻 Wiki只要在终端里敲c deploy --help就能看到每个参数的含义和示例。5.3 我踩过的最后一个坑默认值很多时候某个参数在接口层有默认值比如limit默认 10。你很容易在配置里写上default: 10。但实际场景里用户可能想传一个“没有值”从而让服务端用接口默认值。于是你会发现CLI-Anything 只要检测到default就一定会把10填进去那用户永远无法触发接口默认值。我的解决方案是允许参数值为null。配置里如果写default: null框架就会认为用户没传这个参数适配器会跳过它。这个设计违反直觉但非常实用。如果你也遇到类似问题先检查配置文件里是不是不小心给了默认值。最后分享一个我常用的工作流我现在处理内部事务最舒服的状态是把所有常用入口塞进一个c命令。查日志用c logs --service api --tail 100查订单用c query-order --order-id xxx发版用c release --env staging --tag v1.2.3。看起来不过是一个命令前缀的变化实际体验上却像把一个杂物间改造成了分门别类的工具柜。如果你也想做一套自己的 CLI-Anything我的建议是从一个最让你反复操作的接口开始先把它封成命令再用一周时间把它变成日常操作的一部分。等你自己体会到“不用切浏览器、不用记路径”的爽感之后自然就会想扩展第二、第三个命令。框架可以很简单真正难的是你有没有认真梳理过自己日常操作里的重复劳动并愿意花半天时间去终结它。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。