“能不能给我一个命令行工具”——这是团队里运维同事最常甩给我的一句话。折腾过内部系统的人都有同感后端接口写完了前端页面搭好了结果运营、运维、测试那边还是更习惯敲命令。与其为每个内部工具单独写一套CLI不如用一个通用方案把所有东西都“包”成命令行。这个叫CLI-Anything 的开源项目主打的就是把任意脚本、HTTP 接口、甚至 SQL 查询用一套声明式配置变成可执行命令行工具。我最近在团队内部彻底试了一轮收获不少这篇就把我的实践过程、踩坑记录和一些设计思路完整摊开讲。1. 为什么说“万物皆可 CLI”CLI-Anything 的定位与价值1.1 从“不想打开浏览器”的同事说起先说个真实场景。我们组之前有个数据平台Web 端做了权限控制、可视化报表、一键导出一整套按理说挺完善。但业务那边每天要跑重复性查询他们根本不打开网页而是习惯在终端里敲几行命令把结果直接重定向到文件里继续处理。他们经常抱怨为什么你们后端接口那么全却不给我留一个终端入口后来我明白了CLI 不是过时的东西它反而是自动化链路里最稳定的“连接器”。脚本调用、CI/CD 流水线、cron 定时任务这些场景里你不可能打开浏览器去点按钮。这就是 CLI-Anything 出现的背景。它不重新发明轮子而是把“把一个东西变成命令”的过程标准化。传统做法是每个项目单独引入 argparse 或者 commander手写参数解析、帮助文档、错误提示再为不同语言各来一套。CLI-Anything 换了个思路你只需要提供一份描述文件把要执行的函数、脚本、接口、甚至一段查询语句声明清楚它就能自动生成一个带参数解析、类型校验、自动补全、帮助信息的命令行入口。1.2 CLI-Anything 到底做了什么从命名就能看出它的野心Anything。它想统一的是“一切可以被命令行承载的操作”。我实际测试下来它核心解决了四个层面的问题。第一参数解析自动化。你不需要手动写--foo这种参数定义只需要声明这个命令接收哪些参数、每个参数是什么类型。CLI-Anything 会自动生成--help、校验必填项、把字符串转成整数或布尔值、支持--flagvalue和--flag value两种写法。第二执行体与 CLI 层解耦。命令背后可以是一个 Python 函数、一段 shell 脚本、一个 REST API 调用、一条 SQL甚至可以是一个 Docker 容器。第三统一输出格式。所有命令的 stdout 和 stderr 都是结构化文本方便其他工具继续消费。第四扩展能力。它提供插件接口可以把公司内部的身份认证、日志系统、配置中心都接进去。用一个类比来解释如果普通 CLI 框架是“给你一套积木让你自己搭房子”CLI-Anything 就是“你只需要画一张图纸它自动把房子搭好”。图纸就是那份声明式配置房子就是你想要的命令。1.3 适合谁用如果你是独立开发者手里有几个常用脚本想给它们加一个统一的入口CLI-Anything 可以节省大量样板代码。如果你在团队里负责内部工具经常被要求“把某某功能做成命令行”这个工具能让你从重复劳动里解放出来。如果你做运维自动化需要把一堆 curl、Python 脚本、数据库操作封装成可复用命令CLI-Anything 的设计思路几乎就是为你量身定做的。但要注意它并不是要取代 Click、Typer、Commander 这些成熟的框架。相反它是在更高一层做抽象——你可以把 Typer 写的命令、Commander 写的命令都通过 CLI-Anything 的配置汇总到一个统一的命令门户里。这一点我觉得是它最有价值的地方。2. 核心机制拆解它怎么把“Anything”变成 CLI2.1 从函数签名生成参数——零配置的核心CLI-Anything 最让我惊喜的一点是它能直接读取目标函数的签名来生成命令行参数。比如我定义了一个 Python 函数def report(user_id: int, days: int 7, format: str csv): ...CLI-Anything 会自动识别出user_id是必填的整数参数days是带默认值的整型参数format是带默认值的字符串参数。于是生成的命令行自然就是cli report --user_id 1024 --days 14 --format json这个过程不需要写任何参数解析代码。它背后做的事情其实并不神秘通过反射拿到函数的参数名、类型注解、默认值再把这些信息映射成 CLI 的元数据。如果你了解过 Python 的inspect.signature就能理解它的原理。这种零配置设计对使用者的要求很低但对工具本身的约束很高——它必须处理好各种边界情况比如参数默认值是None怎么办、*args怎么表达、布尔标志位是否需要支持--flag/--no-flag两种形式。我实测发现CLI-Anything 在类型推断上做得比较聪明。它会看类型注解如果没有注解就根据默认值来猜如果连默认值也没有就统一按字符串处理再把字符串转成目标类型。这看起来简单但实际做起来很考验细节尤其是bool类型。--verbose false这种写法看着别扭CLI-Anything 会自动支持--verbose真和--no-verbose假这对互斥标志这点比很多成熟的框架都贴心。2.2 声明式配置把任意操作变成“命令字典”函数签名映射只是第一种模式。更多时候你想封装的不是一个直白的函数而是一串操作流程。比如“从配置中心拉灰度标记调用内部搜索接口结果转成 JSON 写文件”。这种流程用函数签名就表达不了了。CLI-Anything 提供了一套 YAML 格式的声明式配置我称之为“命令字典”。commands: fetch-data: description: 拉取指定业务线的数据并保存 parameters: - name: biz type: string required: true choices: [order, user, sku] - name: output type: path default: ./data.json steps: - action: http.request url: https://api.internal.example.com/v1/biz/{biz}/data method: GET headers: Authorization: Bearer ${AUTH_TOKEN} - action: file.write path: {output} content: ${RESPONSE_BODY}这套配置的含义很直观定义一个fetch-data子命令接收biz和output两个参数然后依次执行 HTTP 请求和文件写入两个步骤。步骤之间通过RESPONSE_BODY这样的内置变量传递数据。我刚开始觉得这种配置有点“重”但用了几次之后发现它最大的优势是同一个配置文件里可以塞下几十个命令而且谁都能看懂这段配置在干什么不需要阅读一堆源码。2.3 扩展机制当内置动作不够用的时候CLI-Anything 内置的动作有 HTTP 请求、文件读写、Shell 执行、SQL 查询基本覆盖了我八成的需求。但剩下两成比如调一个特殊的消息队列 SDK就必须写扩展。它的扩展机制叫“自定义动作”你可以把任意函数注册成一个动作然后在 YAML 配置里直接引用。from cli_anything import register_action register_action(mq.send) def mq_send(queue: str, payload: dict, timeout: int 3): ...注册之后配置里就能写- action: mq.send queue: audit_log payload: event: ${args.event}这个设计很像 Ansible 的 module 机制。扩展动作在 CLI-Anything 内部被类型化地管理它会自动检查参数类型、生成帮助文档还能在 action 执行失败时提供统一的错误包装。写第一个扩展的时候确实有点门槛但好处是写完即被整个团队复用——同事写新命令时只需要在配置里引用不需要知道底层怎么实现。2.4 统一输出日志与调试模式命令行工具做得多了会发现真正劝退使用者的往往不是参数复杂而是出错信息难懂。CLI-Anything 在这个方面做了一个很实用的设计所有命令的日志都走同一个通道并且区分info、warning、error三级输出。平时运行只打印关键步骤加--debug之后才打印每一步的详细参数和耗时。这个机制用 Rails 生态的话说就是“约定优于配置”让不同命令的错误排查方式完全相同。我特别喜欢它的一点是当某个步骤失败时它会打印出当时的上下文快照比如 HTTP 请求的 URL、请求头脱敏后的、响应状态码、上一步的输出片段。这在排查复杂依赖链时价值巨大。传统脚本一旦出错就是一堆堆栈你得重新跑一遍才知道它到底用哪个参数调了什么接口而 CLI-Anything 把“当时发生了什么”直接贴在你面前。3. 实操演示用 CLI-Anything 把三个场景变成命令行3.1 把 Python 函数封装成“报告生成命令”先从一个最常见的场景开始你手头有一堆数据处理函数想把它们暴露给同事用。用 CLI-Anything 最直接的方式是写一个入口文件把函数注册进去。# main.py from cli_anything import app from .reporting import build_report app.register(build_report) if __name__ __main__: app.run()build_report的签名是def build_report( biz: str, date: str, metrics: list[str] None, verbose: bool False ) - str: ...这里有个细节list[str]类型参数怎么通过命令行传CLI-Anything 支持逗号分隔的写法--metrics uv,pv,click它自动会拆成列表。如果没有这个功能用户就得传一个 JSON 字符串体验很糟糕。实测下来这个拆分逻辑符合直觉参数值里如果带逗号怎么办我也遇到过它的处理方式是允许转义\,虽然很少用但至少不会出 bug。运行效果$ python main.py report --biz order --date 2025-03-20 --metrics uv,pv --verbose [info] params: bizorder date2025-03-20 metrics[uv,pv] verboseTrue [info] report generated: ./output/order_2025-03-20.csv这个命令本质上就是把build_report这个函数变成了终端可访问的入口。函数里如果有printCLI-Anything 会把 stdout 重定向到它自己的输出通道避免污染最终命令结果。所以我们在写被封装函数时最好用logging而不是print这样结构化输出和调试日志才能分离。3.2 把 REST API 封装成本地批量工具第二个场景来自一个真实需求平台提供了订单查询 REST API但业务同学不想写 curl。我用 CLI-Anything 的 HTTP 动作写了一个命令commands: query-order: description: 查询订单详细信息 parameters: - name: order_id type: string required: true validator: ^[A-Z]{2}\\d{8}$ - name: fields type: list default: [order_id, amount, status] steps: - action: http.request url: https://gateway.internal/api/v1/orders/{order_id} method: GET headers: Authorization: Bearer ${AUTH_TOKEN} query: fields: {join(fields, ,)} - action: stdout.write content: ${RESPONSE_BODY}我故意在里面加了一个validator字段用来校验order_id的格式。这是 CLI-Anything 一个比较贴心的功能参数除了有类型还能配正则校验器。如果格式不对命令会直接报错而不是把请求发出去再拿一个 4xx 回来。等于是把一部分业务校验前置到参数层了。这个场景我在内部跑了两个月使用率比预期高很多。同事不再需要打开 Swagger、复制 token、拼curl -H Authorization: Bearer xxx而是直接敲cli query-order --order_id AB12345678。验证凭证的问题我用环境变量解决把${AUTH_TOKEN}指向 shell 里的一个变量配合dotenv自动加载就没有泄露 token 到配置仓库的风险。3.3 把 SQL 查询变成只读数据命令数据库操作是另一个高频需求。CLI-Anything 允许你在配置里定义只读 SQL强制使用SELECT语句防止手滑写上DELETE。这是在安全层面做得比较好的一个例子。commands: select-active-users: description: 查询最近 N 天活跃用户 parameters: - name: days type: int default: 7 - name: limit type: int default: 100 steps: - action: sql.query dsn: ${WAREHOUSE_DSN} query: | SELECT user_id, first_active_at, last_active_at FROM dim_users WHERE last_active_at CURRENT_DATE - INTERVAL {days} DAY ORDER BY last_active_at DESC LIMIT {limit} - action: stdout.write content: ${RESULT_CSV}注意我把days和limit直接拼到了 SQL 字符串里这其实有一定风险。CLI-Anything 提供参数化查询语法但这里为了演示就用了简化写法。实操时我建议使用:days这种绑定变量避免 SQL 注入特别是当命令要被多人分享时。3.4 帮助文档与自动补全让工具“会用”CLI-Anything 会自动为每条命令生成帮助信息原理是把配置里的description、参数名、类型、默认值拼成一份对齐的文档。实测效果如下$ cli query-order --help NAME query-order - 查询订单详细信息 USAGE cli query-order --order_id string [--fields list] OPTIONS --order_id string required 订单ID格式如 AB12345678 --fields list default: [order_id, amount, status]相比手写 argparse 的帮助文本这份自动生成的帮助信息最大的优势在于不会和实际参数脱节。不少手写 CLI 经常出现“帮助里写的参数在代码里不存在”的情况而 CLi-Anything 完全基于元数据生成永远不会出现这种偏差。自动补全方面CLI-Anything 支持输出 shell 补全脚本支持 bash 和 zshcli completion bash /etc/bash_completion.d/cli我实际体验下来补全效果主要依赖子命令和参数的元数据。对于choices枚举型参数它能自动列出可选项这是最实用的一点。不过要注意如果总命令数超过 50 个首次加载补全脚本会有轻微延迟后面用缓存解决。4. 常见问题与排查技巧实录4.1 类型推断不准怎么办CLI-Anything 在自动推断参数类型时偶尔会翻车。最典型的是id字段如果函数里写的注解是id: str但实际值是纯数字工具就没法帮你转成 int。这不算 bug但使用时要养成写类型注解的习惯。如果遇到确实没法用注解表达的情况比如参数需要“从两个文件中至少选一个”我会在配置里手工指定validator_type: file或者直接用自定义动作做前置校验。另外一个常见的问题是“默认值参差不齐”。我习惯把默认值全部设为None然后在函数内部判断。但这会导致 CLI-Anything 生成的帮助文档里没有可读的默认值信息用户不知道不传这个参数会发生什么。现在我的建议是默认值能写具体就写具体比如limit: int 100就比limit: int None友好得多帮助文档里直接就能看到。4.2 命令多了以后启动变慢CLI-Anything 的一个设计特点是懒加载app.run()实际只会去解析当前要执行的命令不会加载所有命令对应的函数体。这个设计让单个命令启动保持在 300ms 以内我测下来感触很深。如果你发现启动变慢通常是配置文件里写了太多的大体积自定义动作而且全被顶层 import 了。排查方法很简单加--profile参数它会打印每个扩展动作的加载耗时。我之前就发现一个 SDK 的 import 要花 1.2 秒后来改成在register_action装饰器内部延迟 import启动时间瞬间降下来。这里有个小技巧自定义动作的注册函数里只做名字声明真正的第三方库 import 放到动作执行函数内部既不破坏注册逻辑又能显著加快启动。4.3 与 CI/CD 流水线集成CLI-Anything 结构化输出的优势在 CI 里体现得很明显。我们在 GitLab CI 里跑数据任务时不再拼接一段又一段的 shell 和 Python而是直接调用配置好的 CLI 命令日志统一走 stdout失败时非零退出码能直接被流水线捕捉。一个可以“抄作业”的示例job: script: - cli fetch-data --biz order --output ./data.json - cli validate-file --path ./data.json这里要注意退出码的规范性。CLI-Anything 内部执行用户函数时如果函数抛出了异常默认会打印堆栈并返回 1。但如果我在函数内部sys.exit(0)CLI-Anything 不会拦截这可能导致“命令内部逻辑失败但退出码是 0”的情况。我踩过这个坑之后现在约定所有被封装函数都不直接调用sys.exit而是抛业务异常由 CLI-Anything 统一映射为错误退出。4.4 注意事项与避坑清单先说安全CLI-Anything 支持在配置里引用环境变量这是很必要的但不要把敏感信息直接写进 YAML。特别是dsn、token这些字段一律用${ENV_VAR}写法。如果公司有配置中心可以写一个自定义动作启动时自动拉取配置这样连环境变量都不用手工维护了。再说幂等性。很多 CLI 命令会被 cron 或者 CI 反复执行所以封装时尽量做到可重入。比如生成报告的命令最好在函数内部先把写临时文件再原子替换目标文件避免任务中断导致留下半个文件。参数校验也要“宁严勿松”能加choices就加choices能加正则就加正则让错误尽可能在参数层暴露而不是等到执行到第三步才发现地址写错了。最后一个提醒命令别名。CLI-Anything 允许给命令加短别名比如fetch-data弄成fd。这个功能方便是方便但它会让帮助文档和使用者习惯产生分裂。团队协作时最好是完整命令加别名同时保留文档里始终写完整命令避免新人照着文档敲fd结果发现自己的环境没配别名。5. 从“把一个工具变成命令”到“用命令组织工作流”用了一段时间之后我对 CLI-Anything 的看法已经从“一个生成 CLI 的工具”变成了“一套把操作组织成工作流的框架”。它最有价值的地方不光是省掉参数解析的样板代码而是让团队形成了一种共识任何重复性操作都应该有一个被记录、被复用、被自动化调用的命令入口。我经常跟同事说CLI-Anything 就像给你所有零散的工具配了一个统一的遥控器真正重要不是遥控器本身而是你终于愿意把所有操作整理成一个列表。我个人的一个小习惯是每搭建一个新内部服务第一周就顺手把最常用的三个操作封装成 CLI 命令。三个命令的配置量很小但收益很快就能看到——原本需要指导别人“先 curl 这个、再改那个字段”现在只需要说一句“跑cli sync-data --env staging”。这个体验上的提升只有真正做过内部工具的人才能体会到。如果你也想试试我的建议是先不要急着把整个系统都迁过来。挑一个你每天都要手动重复的命令开始。等你尝到“敲一个命令就把事办了”的甜头自然会想把下一个工具也纳入进来。这也是 CLI-Anything 名字里“Anything”想表达的意思——每一次封装都是把不确定的人为操作变成确定性的自动化资产。
阅读完成 · 觉得有帮助?