先说结论我最近一直在用自己搭的一套小工具项目名就叫CLI-Anything。核心就一句话——把那些只有网页端、只有 API、只有一段很难记的长命令、甚至只有某个同事电脑上才能跑的功能统一收敛成终端里的子命令什么时候想用就cli-anything run xxx一下。这套东西解决的是很多人可能都踩过的坑手边维护着十几个不伦不类的脚本入口五花八门参数格式还都不统一换一个人来操作完全不敢碰。CLI-Anything 最合适的对象是后端开发者、运维、以及其他经常跟命令行打交道的技术同学如果你只是偶尔用两三个命令那直接写 Makefile 就够了没必要上框架。但一旦你的“工具列表”开始超过十几个并且需要给别人复用这个思路就非常值。1. 我为什么要做 CLI-Anything先拆需求再动手1.1 真正的痛点不是“没有命令行”而是入口太散我在做这件事之前身边已经有大量“半成品工具”有的同事用 shell 脚本封装接口有的用 Python 直接requests.post还有一个服务是只有 Web UI 的平时查数据全靠鼠标点。每个工具单独看都不复杂但放到一起就很痛苦。想查用户列表要先想起来去哪个目录执行哪个脚本想触发一次数据同步得翻聊天记录找带--env参数的那条命令想让新同事上手又得口述十几分钟“哪个脚本干什么用”。这个阶段最缺的其实不是“再写一个工具”而是一个统一的入口和一套统一的注册方式。CLI-Anything 最初就是从这个需求长出来的把每个后端能力声明成一个命令剩下的参数解析、帮助文本、输出格式、错误处理全部交给框架统一处理。1.2 设计目标声明式注册、可插拔执行、开箱即用动手前我给自己定了几条硬性标准免得做着做着又变成一个普通脚本合集。第一注册一个命令不能需要写代码。最理想的形态是往 YAML 文件里加一段描述立刻就能在命令行里用起来。这样不只是开发者能维护连偏运维、偏业务的同学也能看懂并贡献命令。第二执行后端必须能接各种类型。同一个终端入口背后既能跑本地 shell 命令也能调 HTTP API还能执行 Python 函数甚至查数据库。这就是名字里 “Anything” 的来历不是某个特定工具而是“什么都能接”。第三默认体验必须够用。自动生成 help 文本、参数校验、超时设置、JSON/YAML/表格等输出格式这些全部开箱即有不该让使用方重复造轮子。1.3 同样解决入口问题CLI-Anything 和别人差在哪其实很多人会问我维护一堆脚本、用 Makefile、或者随便写个 Python 入口不也能收敛命令吗确实能但代价不一样。方案新增一个入口的代价参数校验/帮助可编排适合规模一堆独立 shell 脚本反复写解析逻辑基本没有难几个脚本Makefile较低弱一般少到中型自己写 Python 入口需要代码开发自己实现一般中大型自动化平台重要部署和权限看平台强团队级平台CLI-Anything加一段 YAML自动较好个人/团队我用它替换掉原来的脚本目录之后最直观的感受是新命令的“上手指引成本”变得很低因为 help 是统一的输出是统一的连环境变量的加载方式都是统一的。你不需要再学习每个脚本自己约定的-p到底是 port 还是 password。2. CLI-Anything 的架构思路与核心原理2.1 一条命令在 CLI-Anything 里是怎么跑起来的先理解整体数据流后面看配置就不会懵。执行一条cli-anything run user_list时内部大致做五件事读取入口处配置的 manifest 文件扫描里面所有命令定义。根据命令名找到对应的定义解析用户在命令行传进来的位置参数和可选参数。把参数按定义里的映射关系注入到指定的 provider 上。真正执行后端动作比如调一次 HTTP API、跑一条 shell 命令或者调一个 Python 函数。把返回结果交给统一输出层决定打出一张表格、一段 JSON还是一行纯文本。这个流程听起来简单但核心在于“执行后端”这层被抽象成了协议。命令定义里只写provider: http、provider: exec、provider: python具体的差异由对应 provider 处理。这就像手机的充电接口统一成了一个口至于背后是水电还是火电你不需要关心。2.2 manifest 声明格式是整个框架的主心骨CLI-Anything 的默认配置文件叫manifest.yaml结构很直观。下面是一个最简例子schema_version: 1 defaults: timeout: 30 output: table commands: ping_host: help: 测试目标主机连通性 provider: exec args: - name: host required: true help: 要 ping 的地址 options: - name: count short: c default: 4 help: ping 的次数 run: ping -c {count} {host} user_list: help: 查询用户列表 provider: http method: GET endpoint: https://api.example.com/users headers_env: API_TOKEN output: table每个命令字段不多但信息密度很高。args定义位置参数options定义--xxx形式的可选参数help用来生成统一帮助文本provider决定执行方式。真正新增一个命令时大多数情况下只需要照着现有条目抄一段。我在这里特别强调defaults的作用它定义全局默认值比如默认超时 30 秒、默认输出表格。这保证大家写出来的命令行为一致不因为某个人忘了加--timeout就变成不同的体验。2.3 provider 协议给“Anything”留出扩展位provider 是 CLI-Anything 里最值得展开讲的一层。我目前常用的有四类exec provider执行本地命令。最直接适合包一层docker、kubectl、git等已有命令。http provider发送 HTTP 请求。适合把只有 API 没有 CLI 的服务变成子命令。python provider动态 import 一个 Python 函数并调用。适合逻辑比较复杂、不能靠命令拼字符串的场景。db provider执行带绑定参数的 SQL。适合把常用查询沉淀成命令。之所以设计成协议而不是把所有逻辑写死在一个文件里是因为“编排命令”和“执行命令”的本质区别。如果你想接入一套新的后端比如以后要调用某个 RPC 服务只需要再写一个 provider不需要动框架本身的参数解析和输出逻辑。这就是把扩展点留在正确的位置上否则每接一个新后端就要改主流程迟早会崩。2.4 为什么选择 YAML 而不是直接把命令写成代码有人可能不理解既然底层是 Python为什么不干脆让人写 Python 函数来注册命令我最开始也是用装饰器写了一大堆命令函数但很快发现两个问题。第一每加一个命令就要动代码、跑测试、再过一遍 review太重了。对内部工具来说命令注册的代价应该低到可以忽略。第二不是所有人都会写 Python但几乎所有人都能看懂一段简单的 YAML。配置文件的读者不只是开发者还有运维、测试甚至偶尔教一下就能上手的业务同学。YAML 也不是完全没毛病缩进错误确实会让人头疼。但配合cli-anything doctor这类校验工具可以提前把格式问题暴露出来。比起每个命令入口的代码风格完全不一样这点代价完全值得。3. 实操过程从零到一把能用的 CLI-Anything3.1 环境准备与初始化CLI-Anything 是基于 Python 3.9 的安装非常简单pip install -U cli-anything装完后先跑一次初始化在当前目录生成一个推荐的项目骨架cli-anything init mytool cd mytool初始化生成的目录结构大概是这样的mytool/ ├── manifest.yaml ├── plugins/ │ ├── __init__.py │ └── hello.py ├── scripts/ │ └── status.sh ├── .env.example └── README.mdmanifest.yaml是命令清单plugins/放 Python provider 用到的模块scripts/放你本来就想维护的本地脚本.env.example是环境变量模板。这个结构不是死规矩但很推荐照着用因为后面团队协作的时候大家找东西的位置是确定的。3.2 第一个命令把 Python 函数变成子命令先用最熟的语言来验证最核心的链路。在plugins/hello.py里写一个普通函数import random def greet(name: str, formal: bool False): if formal: return f您好{name}。 return fHello, {name}! def randint(min_value: int 1, max_value: int 100): return random.randint(min_value, max_value)然后在manifest.yaml里加两条命令commands: greet: help: 向用户打招呼 provider: python module: plugins.hello function: greet args: - name: name required: true help: 你的名字 options: - name: formal type: bool help: 是否使用正式语气 randint: help: 生成一个随机整数 provider: python module: plugins.hello function: randint执行的时候cli-anything run greet --name 张三 # 输出: Hello, 张三! cli-anything run greet --name 张三 --formal # 输出: 您好张三。 cli-anything run randint --min-value 10 --max-value 99参数名会自动转成 CLI 风格也就是 Python 里的min_value对应命令行里的--min-value。这块不需要额外写代码框架直接把自己的参数解析结果映射到函数签名上。3.3 把只有 HTTP API 的服务接成子命令实际工作中更多时候不是“我们自己写的函数”而是别人的一套 HTTP API。比如要查订单状态原来只能开浏览器或者敲一长串curl现在可以在 manifest 里定义commands: order_status: help: 查询订单状态 provider: http method: GET endpoint: https://api.example.com/orders/{{order_id}} params_map: order_id: order_id headers_env: API_TOKEN output: json定义里的endpoint支持模板变量params_map表示把命令行里的--order-id参数填充到 URL 路径中。headers_env则告诉框架去环境变量里取API_TOKEN作为请求头。调用方不需要知道 token 是怎么放进去的只需要执行cli-anything run order_status --order-id ORD123 --api-token 你的token或者预先在.env里配好API_TOKEN那么命令行里就可以省略。这里有个细节很关键HTTP provider 支持把命令行参数映射到 URL、query、headers、body 四个位置。你可以在命令定义里显式声明query_map、header_map、json_map而不是靠猜。这种显式映射虽然写起来稍微啰嗦但排查问题的时候非常省事——一眼就能看出参数到底去了哪里。3.4 把本地脚本和运维命令也收编进来团队里一定会有几个运维脚本比如清理临时文件、拉取生产日志、重启容器。它们可能已经能用但参数各不相同。用 exec provider 就能把它们统一进 CLI-Anythingcommands: docker_logs: help: 查看容器日志 provider: exec args: - name: container required: true help: 容器名称 options: - name: lines short: n default: 50 help: 显示最近 N 行 run: docker logs {container} --tail {lines}在执行时框架会把container和lines替换进命令字符串。但这里我要强调一个安全习惯exec provider 内部不要把参数直接拼进 shell 字符串而是应该用参数列表执行或者至少对每个参数做转义。我自己实现的时候默认会对所有参数做一次shlex.quote避免传入; rm -rf /之类的字符串造成灾难。这一点排到“常见问题”里还要再讲一遍因为是真的容易翻车。3.5 统一调试入口和自动补全新命令加多了之后最怕的不是调用而是想知道“这个命令到底配置得对不对”。CLI-Anything 给了两个很实用的子命令。doctor用来做环境自检它会检查 manifest 能否解析、所有 provider 对应模块能否导入、端口网络是否可达、环境变量是否缺失。发现错误时不会只说“失败”而是尽量给出是哪一条命令的哪个字段出了问题。completion用来生成 shell 补全脚本因为命令名是来自 manifest 的框架完全知道哪些命令存在cli-anything completion bash /etc/bash_completion.d/cli-anything cli-anything completion zsh ~/.zshrc配好之后敲cli-anything run Tab就能列出所有命令名继续敲参数也能补全。这个体验对前期推广特别有用因为人都是懒惰的如果还要背诵命令名这套系统很快就会被冷落。4. 踩坑实录与排查技巧4.1 最容易被忽视的 exec 注入风险先说我踩过的一个真实坑。最开始做 exec provider我图省事直接写了fping -c {count} {host}然后用os.system执行。结果测试的时候随手传入了一个带空格和分号的字符串命令直接变成了两条。不是每一次都会出事但只要有一次就会让你后悔。后来我改成所有参数原样进入参数数组整体逻辑变成“先定义好命令模板再把每个{place}替换为 shlex.quote 之后的值”。命令里有管道、重定向这种真正需要 shell 解释的语法时再单独允许一个shell: true显式开关。默认不开宁可让用户多写一个脚本文件也不在默认路径上留下危险。注意默认情况下用户传入的参数都应该被视为“不可信数据”不要直接拼进 shell 字符串。如果你在代码里见过subprocessshellTrue f-string 三件套建议立刻改。4.2 输出文本乱码和 Windows 终端问题CLI-Anything 在 Windows 下跑的时候最常见的问题是表格里的中文乱码和python命令不存在。前者的根源大多是 Windows 下 Python 默认使用 GBK 编码而 YAML 文件读取时用的是 UTF-8。我的处理方案是在入口处强制设置环境变量PYTHONUTF81同时所有文件读写都显式指定encodingutf-8。这样至少保证代码层不乱。终端本身如果还是乱码那就把 Windows Terminal 的编码切到 UTF-8或者用chcp 65001切换代码页。“python 命令不存在”则是 Windows 传统艺能有人装的是 Python 启动器py有人只装了 MS Store 版本。CLI-Anything 的 exec provider 在 Windows 上会做一层路径探测先看看python是否存在不存在就尝试py实在不行就引用sys.executable。这个兼容层很薄但能省掉一大批同事的报错。4.3 环境变量和密钥别写进 manifest我见过有人把 token 直接写在 manifest 的endpoint里比如https://api.example.com?tokenabc123。这看起来方便但 manifest 一旦提交到仓库等于把生产密钥泄露给所有有仓库权限的人。正确做法是统一走环境变量。CLI-Anything 支持.env文件加载并且加载顺序是“系统环境变量优先于 .env命令行显式传入的参数优先于所有环境变量”。token 之类的信息要么放在.env且不上传到 Git 仓库要么用系统自己的凭据管理工具比如 macOS 的 Keychain、Windows 的凭据管理器。4.4 长任务卡住不结束怎么办调接口、跑脚本都躲不过一个现实问题任务可能要执行几分钟甚至更久。HTTP provider 默认 30 秒超时但对于真正的大任务这个默认值就不合适了。我的解法是两层第一命令定义里可以覆盖defaults.timeout调大单个命令的超时。第二对于需要现场观测进度而不是单纯等待返回的任务不要走 HTTP provider而是封装成一个 exec provider 指向本地脚本由脚本在终端里直接打印进度。框架会透传输出流不去做缓冲这样用户能实时看到当前跑到哪一步。另外--dry-run模式强烈建议保留。执行 POST 类操作或者会影响生产环境的脚本前先跑一遍 dry-run 看解析出来的参数是否完全正确可以避免很多手滑事故。4.5 常见问题速查表现象可能原因处理方式找不到命令名manifest 没有重新加载重启 CLI-Anything 或确认文件路径正确参数报错 unknown参数名和定义不一致用cli-anything run cmd --help查看帮助exec 命令没输出命令本身不输出到 stdout确认脚本是 print 到 stdout而不是 stderr中文输出乱码Windows 编码问题设置PYTHONUTF81终端切 UTF-8HTTP 401token 没传或已过期检查headers_env对应环境变量provider 模块找不到Python 路径不包含 plugins 目录从项目根目录执行或用--cwd指定超时频繁服务本身慢调大该命令的 timeout团队里有人不会用没有统一帮助入口配好 completion写一页 README 示例4.6 我给你的一条独家建议命令命名要有规律CLI-Anything 不会限制你命令名怎么写但实际推广中命令名混乱是大问题。我的经验是采用“动词_对象_范围”的命名比如list_user_dev、list_user_prod、restart_service_dev。干净、有规律而且和 HTTP API 的路径风格天然对应。别用check_now、do_things这种名字当时觉得短三个月后看就是天书。5. 应用到团队后的落地经验5.1 先把“一个人好用”变成“全组能用”自己用 CLI-Anything 和带一个团队用要求完全不一样。独自使用时manifest 只服务于自己的习惯写错了也无所谓团队使用时命令的第一读者是别人你要考虑别人的心智模型。我实际推的时候做了三件事一是把 manifest 放进 Git 仓库所有命令变更走 MR 评审review 人不只是在看配置也在共同维护“团队操作手册”二是写了一个极简 README只放三四个高频例子其余一律让人用cli-anything run cmd --help自己探索三是每次加新命令必须附带一两个真实使用场景而不是只交一段抽象配置。这样下来新同学几乎不用问人自己敲两遍就能上手。5.2 从“统一入口”走向“自然语言入口”CLI-Anything 做到后期你会慢慢发现它不只是个命令工具箱也是一个很好的“能力索引”。因为所有后端能力都已经有了结构化描述命令名、参数、帮助文本都在 manifest 里下一步完全可以在这个索引之上做一层自然语言路由。我自己在实验的方向是用户直接说“把测试环境的 user 服务重启一下”先利用配置里的命令描述做意图匹配匹配不到再提示最接近的命令。这本质上是用已有的命令定义作为工具描述喂给大模型做函数调用。如果你未来也想做个人 AI 助手CLI-Anything 的 manifest 就是一个现成的 tools schema省掉很多重复标注的功夫。5.3 一点诚恳的收尾建议如果你现在手头已经有三五个脚本我不建议立刻上这套东西如果已经超过十个、并且常常要给别人用CLI-Anything 这类“声明式命令入口”能帮你把维护成本降到很低。先别追求一次性把所有东西接进来挑两个最常用的命令做通验证流程顺手之后再逐步迁移。我自己迁移了大概两周之后的收益非常明显每次需要操作不用再回忆到底是哪个脚本、哪个参数只需要打开终端敲cli-anything run然后 Tab 一下。那种“所有东西都在一个地方”的掌控感才是 CLI-Anything 给我最大的价值。