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

配置驱动CLI-Anything:把一切API与脚本变成命令行

发布时间:2026/9/28 16:11:55

资讯中心
01
ARTICLE

配置驱动CLI-Anything:把一切API与脚本变成命令行

配置驱动CLI-Anything:把一切API与脚本变成命令行
作为一个每天泡在终端里的人Git、Docker、kubectl 这些命令行工具早就成了我的肌肉记忆。可真正让我抓狂的并不是这些本来就该有命令行的东西而是公司内部那一堆只能打开网页点点点的服务查订单要开浏览器发通知要开浏览器看构建状态还要开浏览器。网页流程学习成本高、不能批量、写不进自动化脚本偏偏每个部门又都造了这么一套。于是我就想要是能有一个工具把任意东西都变成命令行把这些散落在网页、API、脚本里的能力统一收编成一条条规整的命令是不是就能从根上解决这个问题这就是我做 CLI-Anything 这个项目的起因。这篇文章不打算讲太多情怀直接把我从需求分析、架构设计、代码实现到踩坑经历完整拆一遍。如果你也面临工具太多、接口太杂、脚本散落各处的处境或者单纯想让日常操作更少依赖鼠标这篇应该能给你一个可以直接动手复制的方案。1. 先算一笔效率账为什么命令太多太乱必须解决有人可能会说不就是没有命令行界面吗多开几个网页而已至于搞个项目吗我把平时真正浪费时间的地方列出来之后发现这问题比想象中严重得多。1.1 三类让人抓狂的没有命令行场景第一类是纯网页后台。运营后台、管理后台、监控面板这类系统往往没有开放 API就算有文档也藏得极深还得申请 token、配权限。为了查一个订单状态我得先打开浏览器找到收藏夹里的地址登录等页面加载再输入订单号最后还要在一堆表格里找状态字段。这一套下来快则半分钟慢则两分钟关键是它还打断了我的工作流——我刚在终端里处理完上一个任务思绪还挂在命令上下文上突然被拽回图形界面状态切换本身就耗神。第二类是散落各处的一次性脚本。团队里每个人手里都有几个祖传脚本有人用 Shell有人用 Python有人用 Node。功能很简单但入口极不统一参数风格也完全不一样有的要交互式输入有的靠环境变量有的直接写死了路径。平时这些脚本还能跑一旦需要把它们串起来做自动化或者交给新同事用立刻就变成灾难。第三类是各家 SaaS 和内部服务的 API SDK。服务本身有 API 是好事可 SDK 的用法五花八门Python 的装一套依赖Node 的又是一套还有些只有 HTTP 接口得靠 curl 加 jq 拼输出。每个 SDK 都要单独记忆怎么初始化、怎么调方法、怎么解析返回值。说实话我记不住那么多朋友的用法我只想记住一条命令。1.2 CLI-Anything 的设计原则聊完痛点我给自己定了三条设计原则后面所有技术选型都围绕这三条走配置即命令新增一条命令不写代码只改配置文件。哪怕是非程序员照着模板也能添加自己的工具。适配器分离命令背后连的是 REST API、本地脚本还是 Python 函数引擎层不关心。这就是Anything的含义——靠适配器去兼容一切而不是为每个场景写死一套逻辑。体验统一不管背后是什么呈现给用户的都应该是同样的参数风格、同样的输出格式、同样的退出码和帮助信息。有了这三条这个项目才不是又造了一个命令行工具的轮子而是一个装轮子的底盘。2. 整体架构配置驱动的命令注册中心加一圈适配器CLI-Anything 的架构其实非常简单一句话概括读配置注册命令按适配器执行统一格式化结果。但在实现之前我得先把一条命令的一生理清楚。2.1 一条命令的生命周期用户敲下anything order_query --order-id 123456之后引擎内部要依次经历这么几步解析全局入口参数识别出要执行的命令名order_query。从命令注册中心查到这条命令的元信息用什么适配器、需要哪些参数、参数类型是什么。把用户传入的参数交给适配器去执行。适配器返回一个结构化的结果可能是 JSON、退出码、文本。引擎按该命令配置的output格式做最终展示并设置进程退出码。这里面最核心的是第 2 步和第 3 步。命令注册中心是一个在启动时从 YAML 配置文件加载出来的映射表适配器则是关乎执行方式的策略组件。我把这个关系画在脑子里就是命令表是中枢适配器是触手配置把二者绑起来。2.2 命令描述文件Schema设计命令描述文件是 CLI-Anything 的灵魂。我用 YAML 写配置因为相比 JSONYAML 更耐看还支持注释。一条命令的完整描述长这样commands: order_query: description: 查询订单状态 adapter: rest method: GET endpoint: https://api.example.com/orders/{order_id} headers: Accept: application/json params: order_id: type: string prompt: 请输入订单号 required: true restart_service: description: 重启指定服务 adapter: script shell: bash script: | systemctl restart {service_name} systemctl status {service_name} params: service_name: type: string required: true我把配置项拆成几个模块adapter决定执行引擎endpoint/script/module这些是适配器自身的参数params声明用户需要提供的入参。入参的type不只是做类型转换还会直接影响命令行参数的解析方式比如int类型的参数用户传进来就是整数不需要在脚本里再做一遍int(args[id])。2.3 适配器协议与扩展方式适配器本质上就是一个函数对象输入是命令描述 用户参数输出是结构化结果。我给所有适配器定了一个统一的协议方法execute并用一个注册表来管理# adapters/base.py class BaseAdapter: name base def __init__(self): self.registry {} def execute(self, spec: dict, args: dict): raise NotImplementedError def validate_spec(self, spec: dict): 在注册命令时对配置做静态校验尽早发现写错的配置。 keys {endpoint, script, module} if set(spec.keys()) keys set(): raise ValueError(fadapter {self.name} 无法识别配置: {list(spec.keys())})目前我内置了三个适配器rest负责 HTTP 请求script负责跑本地 Shell/Python/Node 脚本python负责动态调用 Python 模块里的函数。下面是这三个适配器的横向对比适配器适用场景依赖学习成本执行模型rest有 HTTP 接口的服务httpx低同步请求script已有脚本、系统命令subprocess低子进程python直接复用 Python 函数importlib中同进程调用以后如果要支持 SSH 远程执行、数据库查询我只需要再写一个ssh适配器、一个sql适配器并在注册表里挂上就行引擎主体代码完全不用动。这也是我用适配器而不是功能模块来组织代码的原因。3. 用 Python 实现一个可动态注册命令的引擎聊完设计进入代码环节。项目主体我用 Python 3.11 实现命令行解析库最终选了 Click而不是现在更流行的 Typer。这个选择值得先解释一下。3.1 为什么选 Click 而不是 TyperTyper 的确写起来很爽装饰器一加就完事但它本质上是基于类型注解来静态生成命令的。CLI-Anything 的命令是运行时从 YAML 里加载出来的命令名和参数在启动之前根本不存在用 Typer 来做动态命令会绕很多弯子。Click 则天然支持这一步它的Command和Option都是普通对象你可以程序化地构造命令并注册到Group上不需要任何装饰器。这正好切中配置即命令的设计目标。3.2 动态命令注册的关键代码整个引擎的入口类非常薄核心逻辑在启动时构建命令表# engine.py from pathlib import Path import click import yaml from adapters import RESTAdapter, ScriptAdapter, PythonAdapter class CLIAnything: def __init__(self, config_path: Path): self.config yaml.safe_load(config_path.read_text(encodingutf-8)) self.adapters_map { rest: RESTAdapter(), script: ScriptAdapter(), python: PythonAdapter(), } def build_command(self, name: str, spec: dict) - click.Command: params [] for pname, pmeta in spec.get(params, {}).items(): option click.Option( param_decls[f--{pname}], promptpmeta.get(prompt), requiredpmeta.get(required, False), typeself._map_type(pmeta.get(type, string)), helppmeta.get(description, ), ) params.append(option) def callback(**kwargs): adapter self.adapters_map[spec[adapter]] result adapter.execute(spec, kwargs) self._emit(spec, result) return 0 cmd_name name.replace(_, -) return click.Command( namecmd_name, callbackcallback, paramsparams, helpspec.get(description, ), ) def register(self, group: click.Group): for name, spec in self.config.get(commands, {}).items(): group.add_command(self.build_command(name, spec))这里有一个细节容易被忽略Click 的命令参数名和 Python 函数参数名必须严格一致否则回调里kwargs拿不到值。由于 YAML 里的参数名可能包含短横线我在构造click.Option时把参数名原样传给param_decls同时在回调里做了kwargs {k.replace(-, _): v for k, v in kwargs.items()}的归一化处理这样不管配置文件里写的是order-id还是order_id最终传给适配器都是统一的 Python 风格键名。_map_type负责把配置里的string、int、float、bool映射到 Click 的类型对象这样用户传参的时候Click 会自动做类型转换和错误提示比自己在回调里硬转省事得多。3.3 参数、提示与输出格式化交互式提示是 CLI-Anything 比较重要的体验点。原则上我希望每条命令既能一键传参直接跑也能不带参数进入交互模式。Click 的prompt参数正好支持这个只要用户没有显式传值Click 就会弹提示等你输入。这让新同事上手时不需要翻文档运行命令跟着提示走就行。输出格式化我做了两层适配器返回的结果默认按 JSON 打印方便机器读取如果配置里指定了output: table引擎会用tabulate渲染成表格适合人眼阅读。同时我保留了一个全局选项--json可以在任何命令上强制输出 JSON。这样同一个命令既能给人看也能给脚本用算是一次适配两种姿势。4. 三个实战案例从 REST 到脚本再到内部函数光讲代码太抽象我拿三个真实的使用场景来演示这里面的命令我都已经在日常工作中用了两三个月。4.1 案例一把订单查询 API 封装成 order 命令我们内部有个订单中心提供标准的 REST API。以前查订单要么开网页后台要么几行 curl 加 jq 拼命令。用 CLI-Anything 之后配置里加了一段commands: order_query: description: 查询订单状态 adapter: rest method: GET endpoint: https://order-center.internal/orders/{order_id} auth: type: token token_env: ORDER_API_TOKEN params: order_id: type: string prompt: 订单号 required: truerest 适配器的实现不复杂核心就是 httpx 请求加异常转换# adapters/rest.py import os import httpx from adapters.base import BaseAdapter class RESTAdapter(BaseAdapter): name rest def execute(self, spec: dict, args: dict): url spec[endpoint].format(**args) headers dict(spec.get(headers, {})) auth spec.get(auth, {}) if auth.get(type) token: token os.environ.get(auth[token_env]) if not token: raise RuntimeError(f环境变量 {auth[token_env]} 未设置) headers[Authorization] fBearer {token} method spec.get(method, GET).upper() request_args { headers: headers, timeout: spec.get(timeout, 10.0), } if method GET: request_args[params] {k: v for k, v in args.items() if k not in spec[endpoint]} else: request_args[json] args resp httpx.request(method, url, **request_args) if resp.status_code 400: raise RuntimeError(f{url} 返回 {resp.status_code}: {resp.text}) return resp.json()以后我需要查订单时直接anything order-query --order-id SO-12345或anything order-query再输入订单号结果从搜索引擎一样的网页流程变成了一行终端输出。更重要的是这个命令可以无缝嵌进别的脚本里做批处理——比如每天早上自动拉一批订单状态生成报表。4.2 案例二把分散的运维脚本收编成一条 sys 命令运维团队有不少祖传脚本比如重启服务、清缓存、查日志。这些东西散落在不同的服务器上命令格式也各不相同。我把其中最常用的几个收编进 CLI-Anything通过script适配器来跑commands: sys_cache_flush: description: 刷新指定环境的缓存 adapter: script shell: bash script: | ssh deploycache-host -C redis-cli -n 3 FLUSHDB echo cache flushed params: env: type: string default: prod这里的包装价值在于用户不需要知道cache-host是谁、命令具体怎么写他们只需要anything sys-cache-flush --env staging。而且我在配置里加了params.env的默认值不传也不会报错降低了使用门槛。4.3 案例三把 Python 模块里的函数变成 calc 命令最实用也最让我惊喜的是python适配器。它允许我直接引用项目里已有的 Python 函数不用写任何胶水代码。比如财务那边有个计算净现值的函数我可以这么配置commands: finance_npv: description: 计算净现值 adapter: python module: finance.utils function: calc_npv params: rate: type: float required: true periods: type: int required: true cashflow: type: float required: truepython 适配器的实现用到了importlib动态导入# adapters/python.py import importlib from adapters.base import BaseAdapter class PythonAdapter(BaseAdapter): name python def execute(self, spec: dict, args: dict): module importlib.import_module(spec[module]) func getattr(module, spec[function]) result func(**args) if isinstance(result, tuple): return dict(zip(spec.get(result_keys, [value]), result)) return {value: result}动态导入一定要防止配置来源不可信这个我放在后面安全章节专门讲。用到这个适配器之后我再也不需要为了暴露一个函数而去单独写一个if __name__ __main__入口也不用给每个工具起一个cli_xxx.py文件配置一行搞定省下的维护成本肉眼可见。5. 落地过程中躲不掉的大坑从能跑到用得舒服中间隔着至少七个坑。我只挑影响最大的四个说每一个都是花了我一两个晚上才填平的。5.1 子进程环境不一致脚本在终端能跑在命令行引擎里就失败第一个坑出现在script适配器上。同一个脚本我在自己的终端里跑得好好的一通过 CLI-Anything 跑就报command not found。查了半天问题出在环境变量上终端登录时会加载.bashrc、.profile里面设置了 PATH 和一堆别名而 CLI-Anything 作为应用被拉起时继承的可能是精简环境PATH 里根本没有那些自定义路径。解决办法不是在脚本里手动 source 一堆文件那样太脆而是在script适配器里做一次环境归一化取当前进程环境并显式补充常用路径同时允许在配置里指定env字段追加需要的变量。我的代码大致是这样import os import subprocess DEFAULT_ENV { **os.environ.copy(), PATH: os.environ.get(PATH, ) :/usr/local/bin:/opt/homebrew/bin:/home/runner/.local/bin, PYTHONUNBUFFERED: 1, } def run_script(spec: dict, args: dict) - dict: script spec[script].format(**args) merged_env {**DEFAULT_ENV, **spec.get(env, {})} proc subprocess.run( script, shellTrue, executablespec.get(shell, bash), envmerged_env, capture_outputTrue, textTrue, ) return {exit_code: proc.returncode, stdout: proc.stdout.strip(), stderr: proc.stderr.strip()}注意script.format(**args)这个做法它把用户的参数直接插进脚本字符串里用起来方便但也埋下了命令注入的隐患。所以我要求任何从params进入脚本的值都必须经过shlex.quote()转义这也是我踩过的另一个坑的教训。5.2 动态命令与 Shell 补全的兼容问题Click 自带 shell 补全机制但它是按照静态命令设计的。CLI-Anything 的命令是启动时才从 YAML 加载的如果用官方文档里那种安装生成脚本的方式补全列表永远是空的——因为生成补全文件时动态命令还没有注册进去。我最终的方案是采用 Click 的懒加载补全思路在 shell 里配置一个每次按 Tab 都会重新生成的补全函数。以 bash 为例我在.bashrc里加了一段_anything_completion() { local IFS$\n COMPREPLY( $(env _CLI_ANYTHING_COMPLETEbash_complete COMP_WORDS${COMP_WORDS[*]} \ COMP_CWORD$COMP_CWORD any_thing 2/dev/null) ) } complete -F _anything_completion any_thing这样每次补全都是实时从引擎输出里拿结果命令新增后立刻就能补全。代价是补全速度比静态脚本慢那么几十毫秒但换来的是改配置即有补全的体验这笔账划算。5.3 配置文件的校验力度太松会崩太严没人用早期我给配置加了一层严格的 JSON Schema 校验要求每个字段都存在、类型完全匹配、不允许多余字段。结果就是团队里根本没人愿意写配置——改一次配置要反复对照 schema错误提示还全是英文长句子。后来我把校验砍到只剩三条命令名不能重复、adapter必须存在于适配器注册表、params里的类型必须在已知类型列表里。其余字段一律宽容处理运行期出错会带着命令名和字段名提示。这让配置新增成本降到照抄已有命令改两行。我也吸取了一个教训配置校验的意义是让错误尽早暴露而不是阻止人们写配置。宁可让奇怪的配置通过前置校验、在运行时给出明确错误也不要让用户卡在配置阶段不知道哪错了。5.4 安全边界不要让Anything变成任意执行这个必须单独说。CLI-Anything 因为设计上就是什么都能跑所以它的安全模型极其重要。我的原则有三条配置来源可信只有团队仓库里的配置文件可以自动加载任何人不能通过环境变量或外部输入注入配置。任何人随便传一个 URL 就能让引擎去执行对方脚本这不可接受。敏感信息走环境变量API token、数据库密码一律不进配置文件配置里只写token_env: ORDER_API_TOKEN这样的环境变量名。这样配置即使被公开也不会泄露凭据。脚本参数强制转义所有进入 shell 脚本的用户参数必须经过shlex.quote()且不允许在脚本里用sudo执行任何来自远程的参数。第一条尤其重要。如果有人能控制你的配置文件那就是拿到了任意代码执行权限对此我心里很有数。6. 边界与下一步什么不该做什么还能做项目做到这个阶段我开始意识到一件事CLI-Anything 不是万能的它也有明确的不该管的边界。把这些边界搞清楚反而让工具更好用。6.1 判断要不要收编进 CLI-Anything 的清单我给自己整理了一个快速判断清单符合任意一条就不应该塞进 CLI-Anything该命令每天使用频率极高且参数复杂、需要自定义逻辑。这种应该写一个独立的专职 CLI 工具而不是包在通用引擎里比如git本身。交互流程极度依赖图形界面比如需要看图表、拖拽、实时预览。强行命令行的结果是体验断层。命令背后对应一个不稳定的外部系统三天两头改接口。除非你能跟上它的变更频率否则你会被适配器维护拖死。依赖非常重安装 CLI-Anything 的机器上无法满足依赖要求。不要为了一条命令搞出一套环境地狱。反过来如果一条命令只是内部服务的一个稳定入口一个已经有脚本偶尔要跑一下的功能一个小团队要共享的查询工具它就很适合用 YAML 配置收编进来。CLI-Anything 的价值是让这些低频、轻量、原本没有统一入口的能力有一个低成本的汇聚点。6.2 可能的演进方向我自己接下来在琢磨几个扩展方向供有兴趣的人参考一是给输出层加一个 TUI 模式。当命令结果是一个表格时终端里直接渲染一个可滚动、可排序的交互表格而不是把几百行 JSON 全部刷屏。二是把适配器的能力从本机扩展到远程。加一个ssh适配器让命令可以透明地在远程服务器上执行这样运维脚本收编的威力会更大。三是计划任务联动。很多命令其实是周期性要跑的比如每小时的巡检、每天早上的报表。CLI-Anything 如果能内置一个 cron 声明把命令定义 调度规则写在一起那它就不再只是一个交互工具而变成了一个轻量自动化平台。最后再分享一个小技巧我在~/.bashrc里给 CLI-Anything 设了一个别名ax命令从anything order-query变成了ax order-query虽然只是少了几个字母但输入频率高的时候舒服程度完全不一样。顺手我还把常用命令做成了 shell 函数比如qo()直接对应ax order-query这就把日常操作压缩到最短路径了。工具做得再多最终还是要回归到顺手两个字。
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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