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

CLI-Anything:统一命令行入口,把脚本、API和系统命令收编成任意命令

发布时间:2026/9/28 21:42:36

资讯中心
01
ARTICLE

CLI-Anything:统一命令行入口,把脚本、API和系统命令收编成任意命令

CLI-Anything:统一命令行入口,把脚本、API和系统命令收编成任意命令
如果你和我一样是整天泡在终端里的人应该能体会到那种“所有事都能用一条命令解决”的快感。CLI-Anything 就是我把这种快感放大到极致的一个小项目一个统一的命令行入口把散落在各处的 Shell 脚本、Python 工具、系统命令、甚至公开 API全部收纳成“任意命令”。这篇文章不打算只讲理念我会把它的设计思路、核心实现、运维经验和踩坑记录全部摊开适合想自建 CLI 工具链、或者正在纠结怎么统一日常命令流的开发者参考看完你基本可以照着重写一套。1. 项目概述与设计思路先把“万物可 CLI”说清楚1.1 命令行到底比 GUI 强在哪先说个自己身上的真实例子。我前几年负责几台Linux服务器的日常巡检当时最常做的动作是SSH 登录、敲df -h、再free -m、然后tail日志眼睛扫完一圈再退出。这个流程熟练之后最快也要一两分钟而且每次巡检都要重复输入几乎一样的命令组合。后来我用一个函数把两三条命令串起来再后来我意识到这个思路可以扩展到「任意事务」。CLI 强在三个层面。第一是效率一条命令直接到结果不需要菜单、窗口、鼠标移动第二是可脚本化CLI 命令天然能被 cron、CI 流水线、shell 脚本调用GUI 做不到这一点第三是可组合ls的输出能给grep用curl的结果能通过管道交给jq处理这个生态几十年来积累得极为丰富。这也是 CLI-Anything 这个项目存在的基础既然命令行效率最高、最易自动化、最方便组合那我为什么不把所有重复性操作全部收编到一个统一的命令入口下于是这个“万物皆可命令行”的项目就诞生了。1.2 CLI-Anything 到底做了什么CLI-Anything 不是一个复杂框架它更像一个“命令收纳盒 命令调度器”。你可以通过一份 YAML 配置文件把任何你想用的 shell 命令包装成有名字、有--help、带参数的子命令也可以直接写一个 Python 函数用装饰器注册成命令。最后统一通过一个anyc入口来调用anyc ping-server --host 8.8.8.8 anyc disk-usage --path /var/lib/docker --top 15 anyc hello --name Tom --loud到了这一步你不需要记住背后那串复杂的原始命令只需要知道“这个功能叫什么名字”剩下的参数问题可以随时通过--help查看。它要解决的核心痛点很明确散落各处的脚本没有统一入口时间一长自己都忘了叫什么复杂的命令组合容易输错参数、记错拼写想分享一条“经验”给同事只能复制一长串命令对方还不一定敢执行想让命令进入定时任务或 CI但脚本入口、退出码、输出格式都不规范。1.3 适用人群与边界如果你用过 tldr、navi 这类工具会发现它们更偏“查询命令手册”而 CLI-Anything 更偏“把命令变成你自己的子命令”。这个项目适合四类人人群典型用途个人开发者把日常开发命令构建、测试、部署等统一成一条命令运维工程师把巡检、排查、日志收集等操作封装成 CLI 子命令小团队负责人把团队共享的操作习惯沉淀到一份 YAML 配置里极客 / 效率控把所有重复性操作变成终端里的一句“咒语”但我得承认它不适合做重型应用如果你的命令本身就是复杂的交互式 TUI 应用或者需要 GUI 图形化呈现那 CLI-Anything 就不太对口。它的定位是“轻量的胶水层”是帮你把重复劳动折叠起来的最后一道包装。2. 技术选型与核心机制为什么选这套组合2.1 CLI 框架横向对比早先这个项目我纠结过一阵子到底是 Python 还是 Node.js 还是 Go。三个生态我都实际体验过简单列个对比方案优势不足最合适场景Python Typer / Click类型提示友好、生态丰富、动态命令成熟需要 Python 环境启动稍慢个人工具链、运维批处理Node.js Commander / oclif前端团队友好npm 生态完善动态参数、类型校验要手动处理前后端一体的团队Go Cobra编译成单一二进制性能最好每加一条命令都要重新编译需要对外分发工具我最终选了 Python Typer底层是 Click为主原因是“配置驱动”这个核心玩法在 Click 上非常成熟Click 的命令组本质是一个click.Group支持在运行时动态添加命令这正好满足我“修改 YAML 就能增加 CLI 命令”的设计诉求。另外 Python 的生态让我能很自然地把 requests、paramiko、openpyxl 这类库用在命令实现里处理 HTTP 请求、SSH 远程执行、Excel 报表等等。2.2 动态命令注册机制CLI-Anything 的核心机制不是预先写死一个命令函数而是运行时分发。它内部维护了一个click.Group启动后读取 YAML 配置把每一条配置翻译成一个标准的click.Command对象然后挂载到 Group 上。这样anyc --help会自动列出所有动态注册的子命令参数说明也会按照 Click 的规则自动生成。这个设计最大的收益是“面向配置开发”。团队里的同学不需要会写 Python只要按格式在commands.yaml里加一段配置就能得到一个新命令。对非程序员背景的运维同事尤其友好。而我自己想实现复杂逻辑的时候再走 Python 函数注册这条路两条通道并不互斥都在同一个入口下共存。2.3 参数传递与模板渲染命令和参数搞清楚了还有个细节配置里的command字符串怎么和真实参数结合我一开始用的是字符串拼接cmdline f{spec[command]} --host {host}很快踩了坑参数值如果带空格或特殊字符很容易被 shell 拆错。后来我改用 Python 的string.Template做占位符替换在配置里写{{host}}代码中统一用safe_substitute填充from string import Template def _render(cmdline: str, values: dict) - str: tpl Template(cmdline.replace({{, ${).replace(}}, })) safe {k: v if v is not None else for k, v in values.items()} return tpl.safe_substitute(safe)这么做的好处是参数占位符策略完全由配置决定缺省参数也能用空串兜底至少不会因为缺少某个值而直接抛异常。至于真正的 shell 转义问题我会让命令以shellTrue执行同时明确一个安全边界配置文件属于可信来源需要由使用者自己承担命令设计时的安全责任不要随手把外部不可信内容拼进命令模板。2.4 加载路径与优先级我设置了两个默认配置目录当前目录下的commands.yaml以及用户主目录下的~/.config/anyc/commands.yaml。加载时优先读当前目录再读用户配置。这个设计参考了现代编辑器“项目配置和全局配置分层”的思路项目里的命令属于这个库或项目全局配置属于个人习惯。配置合并的时候我采用“简单覆盖”策略如果当前目录和全局配置出现同名字命令当前目录的覆盖全局。真实项目里这个优先级很有用比如全局有一个deploy命令但某个项目有特殊的部署方式那就只在这个项目放一份覆盖配置不会污染全局环境。3. 实操过程把 CLI-Anything 搭起来3.1 项目骨架与依赖目标很明确用最少的代码构建一个支持“YAML 声明式命令 Python 函数命令”的 CLI 入口。项目结构如下anyc/ ├── cli_anything.py # CLI 入口与核心逻辑 ├── commands.yaml # 声明式命令配置 ├── pyproject.toml # 设置为可安装的 Python 包 └── README.md # 使用文档依赖只有四个都很常用pip install typer click rich pyyamlclick是 Typer 的底层也是动态命令注册的关键rich负责输出彩色表格信息pyyaml负责解析命令配置。最小可用版可以不用 Typer 只用 Click但 Typer 提供的帮助排版和补全体验确实省事我保留了它。3.2 核心实现入口、动态命令、函数注册代码不长但每一段都有明确职责。先看入口和注册表from pathlib import Path from typing import Callable, Dict, Optional import click import typer import yaml from rich.console import Console from rich.table import Table console Console() app typer.Typer( nameanyc, help万物皆可命令行把脚本、API、系统命令统一成一条命令, no_args_is_helpTrue, ) _REGISTRY: Dict[str, click.Command] {}下面这段是核心它把 YAML 里的一条配置编译成一个click.Commanddef _build_shell_command(name: str, spec: dict) - click.Command: params spec.get(params, {}) def _run(**kwargs): cmdline _render(spec.get(command, ), kwargs) console.print(f[dim]→ {cmdline}[/dim]) proc __import__(subprocess).run(cmdline, shellTrue, textTrue) if proc.returncode ! 0: raise typer.Exit(codeproc.returncode) click_params [] for pname, pcfg in params.items(): click_params.append( click.Option( param_decls(f--{pname},), defaultpcfg.get(default, None), requiredpcfg.get(required, False), helppcfg.get(help, ), ) ) click_params.append( click.Option( param_decls(--silent,), is_flagTrue, defaultFalse, help静默模式不打印实际执行的命令, ) ) return click.Command(namename, callback_run, paramsclick_params, helpspec.get(help, ))这里的关键是click.Command的params字段。我之前用过click.command装饰器然后把参数写死在函数上但动态配置下参数名和数量完全是运行时才知道的所以必须用命令对象去构造参数列表。每条参数支持默认值、必填标记和帮助文本这样--help显示得才像样。动态添加命令的逻辑也简单def _add_command(cmd: click.Command): group: click.Group typer.main.get_command(app) group.add_command(cmd)typer.main.get_command(app)会把你 Typer 应用的命令树转换成 Click 的 Group拿到 Group 之后就能随时往里塞新命令。这一步我用 Typer 的原因是它能自动把 Python 函数的签名转成 Click 参数但真正把命令加进组里的还是原生 Click 的机制。函数式注册我用了一个装饰器def register(name: Optional[str] None, help_text: Optional[str] None): def decorator(func: Callable): cmd_name name or func.__name__.replace(_, -) # 从函数签名生成参数列表 params [] import inspect sig inspect.signature(func) for pname, p in sig.parameters.items(): default p.default if p.default is not inspect.Parameter.empty else None is_flag isinstance(default, bool) if is_flag: params.append(click.Option((f--{pname}/--no-{pname},), defaultdefault)) else: params.append(click.Option((f--{pname},), defaultdefault)) cmd click.Command(namecmd_name, callbackfunc, paramsparams, helphelp_text or func.__doc__) _add_command(cmd) _REGISTRY[cmd_name.lower()] cmd return func return decorator用起来长这样register(namehello, help_text和任意名字打招呼) def say_hello(name: str 世界, loud: bool False): message f你好{name} if loud: message message.upper() console.print(message)当loud参数默认值是布尔值时我把它做成--loud/--no-loud的 flag 形式运行时输入--loud等于传True很符合直觉。3.3 声明式命令配置文件怎么写配置文件的格式是我最常给同事演示的部分。下面这几条都是真实可用的# 例子1ping 主机 ping-server: help: 检查一个主机的连通性 type: shell command: ping -c {{count}} {{host}} params: host: help: 目标主机 IP 或域名 required: true count: help: ping 次数 default: 4 # 例子2查看磁盘空间占用 disk-usage: help: 看某个路径的磁盘占用情况 type: shell command: du -h --max-depth{{depth}} {{path}} 2/dev/null | sort -hr | head -n {{top}} params: path: help: 要统计的路径 default: . depth: help: 递归深度 default: 2 top: help: 显示前几行 default: 10 # 例子3创建目录并初始化 git 仓库 git-new: help: 创建目录并执行 git init type: shell command: mkdir -p {{project}} cd {{project}} git init -b main params: project: help: 项目目录名 required: true注意disk-usage我把du命令的报错输出重定向到/dev/null不然你在 macOS 上会看到一堆权限报错刷屏。这种细节就是配置文件设计者的必修课命令串给机器看的同时也要考虑输出是不是人能看的。3.4 统一的命令列表与帮助任何注册进来的命令anyc --help都会自动列出。我额外加了一个list子命令用表格展示所有命令方便快速浏览app.command(list) def list_cmds(): 以表格形态列出所有命令含动态注册的。 group: click.Group typer.main.get_command(app) table Table(titleCLI-Anything 命令清单) table.add_column(命令, stylecyan) table.add_column(说明, stylegreen) for cname, cmd in group.commands.items(): table.add_row(cname, cmd.help or (无帮助文本)) console.print(table)启动时加载配置然后进入 Typer 主入口def main(): config_file Path(commands.yaml) if not config_file.exists(): home_cfg Path.home() / .config / anyc / commands.yaml if home_cfg.exists(): config_file home_cfg if config_file.exists(): _load_commands(config_file) console.print(f[dim]已加载配置{config_file}[/dim]) app() if __name__ __main__: main()_load_commands就是遍历 YAML 里每条配置调用_build_shell_command再_add_command逻辑非常直白。实际上我的main还会解析--config等参数这里不再展开。3.5 安装与自动补全最省事的安装方式是把项目做成一个可安装的 Python 包。pyproject.toml片段如下[project] name anyc version 0.1.0 requires-python 3.9 dependencies [typer0.9, click8.1, rich13, pyyaml6.0] [project.scripts] anyc cli_anything:main然后在项目目录执行pip install -e . anyc --help # 验证安装 anyc --install-completion # 安装 shell 自动补全Typer 的--install-completion会在.bashrc或.zshrc里配置补全函数。之后你按两下 Tab 就能提示命令名和参数这个体验比我自己手写解析器强太多了强烈建议养成“装完先补全”的习惯。4. 实战场景用 CLI-Anything 折腾真实任务4.1 场景一服务器状态一键体检运维场景最吃这套玩法。以前巡检一堆服务器要在终端里重复敲命令。有了 CLI-Anything我在配置文件里加一个聚合命令把常见的系统状态一次拿到ops-check: help: 一键检查系统负载、内存、磁盘、最近登录 type: shell command: echo --- load ---; uptime; echo --- memory ---; free -m; echo --- disk ---; df -h | head -n {{lines}} params: lines: help: 显示磁盘列表前几行 default: 10每次巡检只需要anyc ops-check一条命令输出还按分隔线分段肉眼扫起来很快。如果想让输出更结构化我甚至会把df -h的结果稍微加工成 JSON 或者表格但那一步通常已经在 Python 函数里实现了YAML 里只放最直接的批量命令。4.2 场景二公开 API 查询变命令另一个很香的使用场景是把 API 封装成命令。比如查询城市天气配置里写一行weather: help: 查询指定城市的当前天气基于 wttr.in type: shell command: curl -s https://wttr.in/{{city}}?format3langzh params: city: help: 城市拼音如 beijing required: true然后执行anyc weather --city beijing就能直接看到结果。当然这里要求网络正常。加上curl之后你甚至可以把返回结果交给jq继续处理。这就是 CLI 的可组合性体现——CLI-Anything 只是入口不限制底层的输出格式。4.3 场景三定时任务与 CI 对接CLI 能进 cron 是它区别于 GUI 的最大优点。我在 crontab 里直接写0 9 * * * cd /opt/anyc /usr/local/bin/anyc ops-check /var/log/anyc-daily.log 21这里要注意一点crontab 环境里通常没有完整的 PATH所以尽量写绝对路径另外命令的退出码要规范如果subprocess.run返回非零我的代码会通过raise typer.Exit(codeproc.returncode)把退出码原样带出cron 和 CI 就能按退出码判断成功还是失败。这个细节很多人容易忽略结果自己的脚本在终端里跑得好好的进了 CI 却怎么都报错其实就是退出码没有透传。CI 流水线里我还会用到--silent参数因为我只关心机器能解析的输出不想看到前面那行装饰性的“执行命令”提示。输出做机器可读处理这一点在自动化场景是刚需。5. 常见问题与排查技巧实录5.1 高频问题速查表下面这些问题是周围同事把项目拿去用之后最常遇到的基本每个都能对号入座问题可能原因解决方法anyc --help里看不到配置里的命令启动时没有找到commands.yaml确认文件在当前目录或~/.config/anyc/下参数值带空格被截断模板替换后没有加引号在command里的占位符前后补上英文引号Tab 补全不生效安装后没有配置 shell执行anyc --install-completion并重新加载 shelldisk-usage这类命令在 macOS 上报不同参数各平台du参数不一致针对平台写不同配置或用 Python 函数实现跨平台逻辑中文输出乱码终端编码问题确保PyYAML读取时用utf-8终端也设为 UTF-8curl命令没有任何输出网络不通或 API 服务异常先用原始命令裸跑一遍定位问题5.2 几条容易忽略但很关键的实战经验第一命令命名要统一成“动词-名词”结构。一开始我随手把命令叫fix、check、deploy后来命令多了光看名字根本猜不出它是干嘛的。改成ops-check、git-new、disk-usage这种结构之后即使不用--help光看列表心里也清楚。第二日志输出要分道。正常情况下stdout放真正的数据stderr放日志和警告。如果你把调试信息一股脑打进stdout后面接jq解析的时候会被干扰。我在代码里会尽量区分这两路输出给进度信息用console.stderr或者--silent做开关。第三配置文件的参数校验别偷懒。有的命令必须提供必填参数我就在 YAML 配置里写required: trueClick 会在执行前自动拦截并提示缺参用户体验明显比“命令跑了一半才发现模板变量没塞进来”好得多。第四模板里的变量一定要兜底。我遇到过一种情况某个参数没传但是命令模板里涉及它最后生成rm -rf {{target}}变成rm -rf这相当危险。所以我在_render里把所有缺失值替换成空串之外还建议配置里的危险命令直接用条件判断比如缺参数就拒绝执行。命令行工具越方便越要小心边界条件。第五不要过度封装。CLI-Anything 适合把“底层的、稳定的、你反复要做的操作”收编成命令不适合把所有临时命令都塞进去。我就曾经过度设计把一条只用了两次的一次性命令也注册进去结果配置越来越臃肿真正常用的命令反而被淹没了。现在我的个人准则是如果一条命令三周之内没用到第二次就把它的配置删掉保持收纳盒干净。这个项目我前后迭代了不少版本最让我意外的是它从一个“自己用的小工具”慢慢变成团队共享的入口很多不会写 Python 的同学自己看着文档就能往commands.yaml里加命令。这说明“配置驱动 CLI”这种设计思路不只是个人的效率工具也可以是一个轻量的团队知识沉淀方式。如果你手上也有一堆散落脚本和写了一半的小工具不妨试试把它收编成一套 CLI-Anything让终端真正变成你的一站式入口。
02
RELATED NEWS

相关资讯

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

03
WHY YAOTU

想打造同款高转化官网?

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

◈

场景化定制

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

◐

营销型架构

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

▲

全周期服务

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

免费获取你的建站方案

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