CLI-Anything:统一命令行入口,收编脚本、函数与API
发布时间:2026/9/28 17:37:36来源:尧图网络
CLI-Anything这个项目名字听起来有点狂但真做起来你会发现所谓“任何东西”都有共性。我去年因为同时在几个项目里来回切换工具今天用curl敲API明天写Python脚本后天还得手动跑cron清理日志烦到极致之后就搭了一个统一命令行入口——CLI-Anything。它不把世界上的软件都重写一遍而是把那些杂乱的shell命令、Python函数、HTTP接口统一收编成一套带参数校验、帮助文档、配置文件和友好输出的CLI工具。这个项目非常适合后端开发、运维、数据工程这类每天跟命令行打交道的人。你不需要会特别高深的技术只要了解基础的Python和Shell就能照着思路自己搭一套。它的核心价值不是让你写出多酷的框架而是把日常那些零散的“一次性脚本”收拢成有规矩、可复用、能交接的工具。说白了就是给你的命令行工作流装一个统一的前台。1. 项目概览CLI-Anything到底在解决什么问题1.1 一句话说清楚CLI-Anything是什么CLI-Anything本质上是一个“命令收编层”。它不自己去实现某个业务功能而是给现有的脚本、函数、HTTP接口一个统一的命令行入口。举个例子。我日常工作的机器上有一堆散落各处的工具有写好的Python数据清洗脚本有负责重启服务的Shell脚本还有几个需要带鉴权token调用的内部API。过去我用它们的方式完全不同有的要进到指定目录去跑有的要记参数顺序有的还要先生成token再拼接URL。这就像家里钥匙一大堆每把钥匙对应一扇门但门上没写名字全凭记忆。CLI-Anything的思路就是给这些“门”统一换锁芯最后你只需要记住一把钥匙。所有任务都通过同一个命令入口触发比如cli-anything run script --name clean_data或者cli-anything call api --name list_users。参数怎么传、是否需要鉴权、输出什么格式都由CLI-Anything替你操心。这个项目适合的受众其实很广。如果你是那种经常需要在终端里敲各种命令的开发者或者团队里总有人把脚本写成“只有自己能看懂”的江湖手艺CLI-Anything就能帮上忙。它不需要团队全员学习新框架只要求每个人了解“通过统一命令入口调用任务”这一个概念。1.2 它到底解决了什么痛点第一个痛点是工具碎片化。真实工作环境里同一个操作经常分散在不同工具里。查数据库要去连MySQL客户端看服务日志要去翻文件部署一次要执行一串Shell命令。每次切换到不同工具都要重新回忆它的参数和语法效率极低。CLI-Anything把这些操作全部收编为一个入口对应的子命令平铺在--help里找起来一目了然。第二个痛点是脚本参数规范缺失。我自己写过太多“裸脚本”参数全靠sys.argv[1]硬取位置记错就报错而且毫无帮助提示。时间一长我自己都忘了第三个参数到底是端口号还是超时时间。CLI-Anything通过参数解析层统一解决这个问题每个任务声明自己的参数类型、默认值和帮助说明调用错误时直接给出清晰报错。第三个痛点是交接成本高。团队里总会有人把自己的知识锁在本地脚本里人一走脚本就报废。但如果你把脚本收编进CLI-Anything每个命令都有注册信息和帮助文档新人看一眼--help就知道怎么调用。这一点在实际协作中价值非常大少了很多“这个脚本怎么跑”的追问。2. 整体设计与技术选型2.1 技术栈选型为什么是PythonTyper我在技术选型上比较了几种方案最终定的组合是Python Typer Rich。第一是生态。团队里本来就用Python写各种自动化脚本收编过来的成本最低。一个已有脚本只要包一层函数就能被CLI-Anything调用不需要重写。如果选Go或Rust性能虽然好但移植存量脚本的工作量太大了。第二是参数解析和帮助文档生成。Python标准库的argparse能用但写起来啰嗦且帮助文本不太好维护。Typer这个库特别适合做CLI外壳它基于类型注解自动生成参数解析、校验和--help文档。你只需要定义函数参数标注类型和默认值Typer就帮你搞定一切。第三是输出体验。Rich能让终端输出带颜色、表格和进度条调试和演示效果都很好。CLI工具最怕黑漆漆一团文字看不清Rich帮我把结构化输出直接打在终端上。我也对比过直接用Shell脚本做总入口的方案发现后期维护很痛苦。Shell没有类型概念参数校验全靠手工分支一多脚本就变成一坨难以阅读的代码。而CLI-Anything作为一个Python项目可以单元测试、可以加日志、可以按模块拆分明显更扛得住项目体量增长。技术栈确定后我做了一个小验证原型把三个不同来源的脚本收编进来测试从写注册信息到实际调用大概花了多久。结果我第一次跑通只用了两个小时这个成本完全值得投入。2.2 三个核心设计原则CLI-Anything能比较稳定地支撑我日常使用靠的是三条设计原则。原则一注册表驱动。所有的收编对象都通过“注册”的方式挂到CLI上而不是在代码里写一堆 if 分支去分发。注册表是一个数据字典记录了每一个命令的名字、帮助文本、适配器类型、目标对象和参数定义。新增一个命令只需要往注册表里加一条记录主分发逻辑完全不用改。这个设计让CLI-Anything保持开闭原则对新增开放对修改关闭。原则二统一输入输出。无论背后是Shell脚本还是HTTP接口用户的输入都只有“命令名参数”。后端则统一返回结构化结果由CLI-Anything负责渲染成表格、JSON或普通文本。这样做的好处是你换掉实现方式时用户侧完全无感。比如原来一个备份脚本用的是Shell实现后来改成Python函数调用命令一条都不用改。原则三适配器隔离。这是最关键的一点。CLI-Anything不直接执行目标命令而是通过适配器Adapter来间接调用。Shell适配器负责执行子进程Python适配器负责调用函数API适配器负责发HTTP请求。每种适配器各自封装一种执行方式新增一种执行方式时不需要动主程序逻辑。2.3 目录结构与模块划分项目结构是我反复调整过的版本看起来清晰扩展也方便。cli_anything/ ├── app.py # CLI入口负责实例化Typer应用 ├── registry.py # 注册表管理存放全部命令元信息 ├── config.py # 配置文件加载全局YAML解析 ├── adapters/ │ ├── __init__.py # 适配器工厂 │ ├── shell.py # Shell脚本适配器 │ ├── python_func.py # Python函数适配器 │ └── http_api.py # HTTP API适配器 ├── jobs/ │ ├── backup.py # 备份任务 │ ├── deploy.py # 部署编排 │ └── health_check.py # 服务检查 ├── config.yaml # 全局配置含鉴权信息、超时等 ├── requirements.txt └── pyproject.toml我把注册表放在registry.py里每个被收编的任务放在jobs/下适配器独立成包。这种分层的目录结构有一个好处当你需要排查某个任务的执行链路时路径非常清晰——入口app.py找命令registry.py查元信息适配器负责执行任务函数放在jobs里。整个流程不绕弯子。3. 核心功能与实现细节3.1 注册表设计让每个子命令都有户口注册表是整个CLI-Anything的大脑。我设计的数据结构不复杂核心是一个列表每个元素是一个命令的元信息。# registry.py from typing import List, Dict _REGISTRY: List[Dict] [] def register(name: str, help_text: str, adapter_type: str, target: str, params: List[Dict] None): 注册一个命令到CLI-Anything。 _REGISTRY.append({ name: name, help: help_text, adapter_type: adapter_type, target: target, params: params or [], }) def get_all_commands() - List[Dict]: 返回全部注册命令。 return _REGISTRY为什么用注册表而不是硬编码路由最开始我写过一个版本在app.py里用一堆if command backup: backup.run()的代码结果是每加一个命令就要改主文件而且主文件越来越大阅读和测试都很痛苦。改成注册表后新增任务只做两件事写任务函数、调用一次register。主分发逻辑始终保持稳定。注册表还有一个隐藏收益因为所有命令元信息都集中在一个数据结构里我可以在外层生成帮助文档、命令列表甚至Zsh补全脚本。这些都是数据驱动的不用为每个命令单独写一份说明。3.2 三种适配器脚本/函数/API一网打尽适配器是CLI-Anything的执行引擎。我目前实现了三种覆盖了日常九成以上的需求。Shell适配器是基础款。它通过subprocess.run执行外部命令支持shellTrue的管道写法也支持纯参数数组传递。# adapters/shell.py import subprocess from typing import List def run_shell(target: str, args: List[str]) - int: 执行Shell命令返回退出码。 cmd [target] args proc subprocess.run(cmd, capture_outputTrue, textTrue, shellFalse) if proc.stdout: print(proc.stdout) if proc.stderr: print(proc.stderr, file__import__(sys).stderr) return proc.returncodePython函数适配器更简单本质上就是“调一个函数”。我把函数名存储在target里运行时通过动态导入找到并调用它。这样做的好处是任务函数可以享受Python生态的库比如pandas处理数据、requests调API业务逻辑不用被CLI层污染。HTTP API适配器是重头戏专门用来把内外部接口封装成命令。它会自动读取配置文件里的base_url和token你只需要指定路径和方法。这样我就可以用cli-anything call api --name create_user --payload {name:tom}来代替一长串curl命令。三种适配器的选择逻辑封装在适配器工厂里CLI层不需要关心对象到底是什么类型只要传入adapter_type即可。新增第四种适配器比如docker容器的远程执行只需要新增一个类并在工厂里注册就能被CLI-Anything使用。3.3 参数解析与动态帮助文档CLI-Anything的参数定义是声明式的存在注册信息里。每个参数包含名称、类型、是否必填、默认值和帮助文本。params [ {name: name, type: str, required: True, help: 用户名}, {name: timeout, type: int, required: False, default: 30, help: 超时时间}, ]Typer支持通过注解生成参数解析但这里的难点是运行时才能确定有哪些参数。我采用动态生成子命令的方式遍历注册表为每个命令创建一个Typer命令函数并把参数定义为可选参数或必需参数。这样用户输入cli-anything run script --help时就能看到这个命令自己的帮助文档。动态帮助文档这个功能极大提升了CLI的可用性。以前脚本参数记不住还要翻源码现在直接看帮助就行。而且因为帮助文档来源于注册信息写注册表时顺手填好help字段就能自动获得完善的文档。3.4 统一输出与退出码规范CLI工具最容易犯的毛病是输出格式混乱。有的脚本print一堆文本有的直接没输出。我在CLI-Anything里统一了输出层普通结果用Rich表格打印需要机器读取时加--output json参数切换到JSON格式。退出码也做了规范这是自动化脚本非常依赖的一点。退出码含义场景0成功任务正常完成1业务失败任务执行时目标对象返回错误2参数错误用户传入了非法参数3目标不存在注册表里找不到对应命令4适配器异常比如网络超时、依赖缺失我之前踩过一个大坑Shell适配器处理完命令后没有把子进程的退出码传出去导致目标脚本抛错了但上层看起来还是成功退出。现在适配器严格执行退出码传递自动化流水线跑出来的结果是可信的。4. 从零构建一个最小可用的CLI-Anything4.1 初始化工程结构先把基础环境搭起来。我建议用venv隔离避免污染系统Python。mkdir cli-anything cd cli-anything python -m venv venv source venv/bin/activate pip install typer rich pyyaml requests然后创建目录结构和上面的Python文件。工程初始化阶段不用写很多代码把pyproject.toml配好让CLI可以被pip以开发模式安装。# pyproject.toml [project] name cli-anything version 0.1.0 requires-python 3.10 [project.scripts] cli-anything cli_anything.app:main [tool.setuptools] packages [cli_anything, cli_anything.adapters, cli_anything.jobs]配置好之后运行pip install -e .终端里就能识别cli-anything命令了。这一步也就是文章开头说的“统一入口”基础。4.2 注册表子命令分发实现接下来实现入口和注册表逻辑。app.py需要动态读取注册表为每个命令生成一个独立的Typer子命令。# app.py import typer from cli_anything.registry import get_all_commands from cli_anything.adapters import run_adapter app typer.Typer() def _make_command(item): 为注册表里的每个命令动态生成一个执行函数。 def command(**kwargs): result run_adapter(item[adapter_type], item[target], kwargs, item.get(params, [])) typer.echo(result) return command app.command() def run(name: str, **kwargs): 运行指定命令。 for item in get_all_commands(): if item[name] name: fn _make_command(item) fn(**kwargs) return typer.echo(f命令 {name} 不存在请检查 --help, errTrue) raise typer.Exit(code3) def main(): app()为了让代码清晰我把动态参数生成放到一个辅助函数里用户实际触发时用cli-anything run backup --target /data即可。初次实现时可以只保留run命令后续再扩展其他入口。4.3 HttpAPI适配器实现HTTP API适配器是实用性最强的部分我重点说一下。它的核心目标是让用户用CLI而不是curl去调用API。# adapters/http_api.py import requests def call_http(base_url: str, path: str, method: str GET, token: str None, params: dict None): 统一HTTP API调用。 headers {} if token: headers[Authorization] fBearer {token} url f{base_url}{path} if method.upper() GET: resp requests.get(url, headersheaders, paramsparams, timeout30) else: resp requests.post(url, headersheaders, jsonparams, timeout30) return resp.status_code, resp.json() if resp.headers.get(content-type, ).startswith(application/json) else resp.text我用这个适配器注册了一个查询GitHub用户信息的命令调用方式很直观。cli-anything call api --name gh_user --param username:octocat # 返回 # 用户名: octocat # 公开仓库数: 8 # 粉丝数: 5823实际操作时要注意base_url和token统一从配置文件读取避免每个API命令都重复写。我把它放在config.yaml里运行时加载到全局配置对象。4.4 任务编排与定时触发CLI-Anything还有一个隐藏玩法任务编排。我最初只想做收编后来发现很多任务是“一串动作”的组合。比如备份任务要先检查目录再压缩文件最后上传归档。与其写一个巨大的Shell脚本不如拆成几个小命令再用一个编排命令串联。# jobs/backup.py from cli_anything.adapters.shell import run_shell def run_backup(source: str, dest: str): run_shell(mkdir -p, [dest]) run_shell(tar, [-czf, f{dest}/backup.tar.gz, source]) run_shell(mv, [f{dest}/backup.tar.gz, f{dest}/backup-final.tar.gz])这样注册到CLI后用户只需要执行cli-anything run script --name backup --param source:/data --param dest:/backup内部三个步骤自动完成。中间的失败检查点在编排函数里处理某一步失败就直接抛异常不会继续执行。定时触发我接入了APScheduler允许把某些命令挂到cron式时间表上。比如每天凌晨两点执行备份就执行cli-anything schedule add --cron 0 2 * * * --command backup。这部分适合放在后台服务里运行权当给CLI扩展了定时能力。4.5 一条命令拉起整套服务最后演示一个综合场景。我经常需要本地启动三个服务后端API、前端静态服务、数据库。过去要开三个终端现在我把它们编排成一个dev group。# jobs/dev.py def start_dev(): from cli_anything.adapters.shell import run_shell import subprocess procs [] procs.append(subprocess.Popen([python, api.py])) procs.append(subprocess.Popen([npm, run, dev])) procs.append(subprocess.Popen([docker, compose, up])) for p in procs: p.wait()注册后一个命令搞定整套环境启动。这里我没有并行捕获日志日志直接打到终端开发时看实时输出反而更直观。这样的编排场景是CLI-Anything真正体现“Anything”价值的地方——它不关心你背后是什么技术栈只要能执行就能被收编。5. 常见问题与排查技巧实录5.1 命令找不到与环境变量我在另一台服务器装上CLI-Anything后发现新开的终端窗口执行cli-anything却报command not found。排查了一下原因是开发模式下pip安装路径没有加入PATH。解决方式有两种。一种是确认venv激活再执行pip install -e .安装时输出会显示脚本安装路径另一种是给命令加一个Python模块入口兜底永远可以用python -m cli_anything触发。# pyproject.toml 增加 [tool.setuptools] scripts [] [project.scripts] cli-anything cli_anything.app:main cli-anything-py cli_anything.app:main更稳妥的方案是把cli-anything做成了软链接到本地bin目录同时在文档里写明“如果找不到命令先执行python -m cli_anything --help”。团队里有人装完忘了激活venv这类问题大概率很快就能定位。5.2 启动太慢懒加载才是本体CLI-Anything跑了一段时间后我发现简单的--help也要等一秒多。原因很简单app.py里导入了所有adapters而adapters里又import了requests、rich、subprocess等一堆模块。实际上帮助命令根本不需要加载这些重型依赖。修复方法是把适配器的导入时机从“启动时”改成“执行时”。注册表里只存字符串标识运行时再动态导入对应模块。# adapters/__init__.py import importlib _ADAPTERS { shell: cli_anything.adapters.shell, python_func: cli_anything.adapters.python_func, http_api: cli_anything.adapters.http_api, } def run_adapter(adapter_type: str, target: str, kwargs: dict, params: list): module importlib.import_module(_ADAPTERS[adapter_type]) func getattr(module, run) return func(target, kwargs, params)改完之后启动时间从大约1.2秒降到0.18秒。实测感受非常明显频繁敲命令时不再有卡顿感。懒加载对CLI工具来说不是优化项而是基本项。5.3 API请求超时与重试HTTP API适配器上线后我发现偶尔会碰到一次性超时的情况。内网服务偶尔抖动直接报错用户会觉得不好用。我给API调用加了重试机制但重试不是无脑加只在GET和幂等POST上启用。import tenacity tenacity.retry( stoptenacity.stop_after_attempt(3), waittenacity.wait_exponential(multiplier1, min1, max10), retrytenacity.retry_if_exception_type(requests.Timeout), ) def _http_get(url, headers, params): resp requests.get(url, headersheaders, paramsparams, timeout20) resp.raise_for_status() return resp十秒钟的重试成本我觉得可以接受。但要用这功能一定要想清楚接口是否幂等否则重复提交订单这类操作会出大问题。我只在查询类命令里开了重试写入类命令明确不重试只报错。5.4 退出码被吞的坑退出码被吞是我很早就遇到过的问题但值得反复强调。ShellAdapter如果用subprocess.run(cmd)后不检查returncode目标命令失败时CLI可能仍然返回0上层自动化流程完全不知道失败。这个问题让一次备份任务在日志里显示“成功”但实际备份文件没生成。排查到最后才发现是退出码传递缺失。现在我的ShellAdapter强制结束sys.exit(proc.returncode)同时把stdout和stderr分开打印错误信息不会被Rich美化得看不出重点。CLI工具对退出码的敬畏应该排在输出美化之前。6. 我的一些个人心得6.1 我踩过最大的坑一开始想设计成插件平台第一版CLI-Anything我用了大量抽象想做成一个插件平台让每个人都能写插件。结果架子搭得很漂亮功能却迟迟没落地最后真正能用的命令没有几条。后来我把需求收敛成“先收编我自己最常用的五个命令”只用了半天就把工具跑了起来。这个教训很深刻先把真实任务跑通再去想抽象和扩展。CLI-Anything现在虽然有很多适配器但我前期是靠“接一个真实需求、沉淀一个适配器”的方式慢慢长出来的。任何工具一开始就追求大而全基本都活不过第一周。6.2 分享一个非常管用的小技巧给CLI-Anything加一个全局--dry-run参数执行任何命令时都只打印“将要执行什么”不真正执行。cli-anything run backup --source /data --dest /backup --dry-run # 输出 # [DRY-RUN] tar -czf /backup/backup.tar.gz /data # [DRY-RUN] mv /backup/backup.tar.gz /backup/backup-final.tar.gz这个参数在调试编排任务时简直救命。以前我改完备份流程要真跑一遍才知道顺序对不对现在dry-run一眼就能看出命令参数有没有传错。我也建议在注册表里增加一个dry_run_safe字段让单个命令自己声明是否支持试运行有些写了就很难回滚的操作比如删除命令默认就禁止dry-run模拟。6.3 后续还能怎么扩展CLI-Anything的下一步我想把注册表导出成JSON这样可以让团队里其他工具消费同一个命令清单同时计划支持远程适配器让CLI转发到另一台机器执行。不过以我现在的经验来看扩展点不在多而是把当前这套收编逻辑做扎实。如果你也想搭一个CLI-Anything我的建议是别从框架开始从你明天就要用到的三条命令开始。把最痛苦的操作收编进来用着顺手了再慢慢加。命令行这东西真正实用的不是复杂的设计而是每次敲命令时减少的那几秒等待。
网站建设高端定制企业官网