CLI-Anything:把一切终端操作都收进命令行这个入口
在终端里泡了十几年之后,我逐渐意识到一个很尴尬的事实:命令行工具越多,操作反而越碎。今天要清日志用什么工具,明天要处理 JSON 用什么,后天要调接口又要记参数。每个工具都有自己的参数体系、输出格式和配置文件,明明都是终端操作,却像在不同国家讲不同方言。
CLI-Anything 这个项目就是冲着这个痛点来的。它的定位很直接——把日常开发中反复出现的终端操作,统一收敛到一个命令行入口里。不管你是要解析 JSON、批量重命名文件、查端口占用、跑 HTTP 请求,还是做文本处理、格式化代码、操作 Git 仓库,都通过一套统一的命令约定和一个可扩展的插件体系来完成,不用再用不同的命令拼接组合解决一个问题。
这篇文章面向两类人:一类是想找一套顺手工具链的开发者,看完可以直接套用核心设计和插件写法;另一类是打算自己封装内部 CLI 工具集的团队,架构思路和踩坑记录可能能帮你少走弯路。我把整个项目从设计思路到落地实现,再到实际跑起来遇到的问题,全部展开讲一遍。
1. 项目概述:CLI-Anything 到底在解决什么问题
1.1 核心需求解析
先说需求。终端操作看似是个老生常谈的话题,但在实际工作里,我见过太多类似的场景:
验证一个 JSON 数据,顺手就用python -m json.tool,但公司服务器上可能没装 Python。写接口联调用curl,一串参数堆下来,URL 里带空格带引号,转义转得人想摔键盘。查端口占用,lsof -i或netstat -ano的语法在不同系统上还不一样。想统一格式化代码,项目里 Prettier、Black、gofmt 各管各的,换个项目就得换一套命令。
CLI-Anything 的思路,是把这些分散的、高频的、语法碎片化的小操作,全部收编到一个统一框架里。你只需要记一套规矩——anything <工具组> <具体操作> [参数],剩下的交给框架去调度。
这个项目最核心的价值不是它内置了哪些工具,而是它定了一套规则:每个功能模块就是一个插件,插件只负责定义"命令名"和"执行逻辑",框架统一负责参数解析、输出格式化、错误处理、配置管理。后续谁想加新功能,写一个插件文件丢进目录,命令就注册上了,不需要改框架本身。
1.2 适用场景与目标用户
什么人适合用这套东西?
场景一:日常开发中的高频琐碎操作。解析 JSON、时间戳转换、URL 编码解码、正则测试、端口查询、进程查找,这些几乎每天都能用到。
场景二:团队内部工具的统一入口。很多团队手里有不少脚本——部署脚本、数据库备份脚本、日志分析脚本,各写各的,入口不统一,新同学入职光记脚本路径就得记半天。用一个统一 CLI 框架把它们包起来,按组分类、统一参数风格,上手成本立刻就下来了。
场景三:个人终端工作流重度用户。如果你跟我一样,一天有一半时间泡在终端里,值得花一下午把自己的高频操作用这个框架整理一遍,长期回报非常大。
场景四:自动化流水线上的命令入口。CI/CD 里的很多步骤本质上都是命令行操作,CLI-Anything 这类工具可以跨语言统一这些调用方式,比直接散落一地的 shell 命令好维护得多。
我在做这个项目时的判断是:它不是要替代现有的专业 CLI 工具,而是充当一个"前台调度层"——专业工具干重活,它负责让你以一致的方式去调用这些能力。
2. 整体架构设计与思路拆解
2.1 框架选型:为什么用插件式而不是单体式
做这类工具,第一个决策是单体式还是插件式。单体式的好处是集成度高、依赖简单,但坏处是:每加一个功能就得动主程序,主程序会越来越臃肿。插件式的核心优势在于功能与框架解耦。
我直接选了插件式。插件的具体形态是:一个目录下一个子目录一个插件,每个插件包含一个配置文件(声明命令名、参数、描述)和实现脚本。框架启动时扫描插件目录、加载注册信息、生成帮助菜单,执行时按命令路由到对应实现。
不过这里有个架构取舍问题:插件用什么语言实现?如果框架用 Node.js,插件也用 Node.js,那公司内部如果只装了 Python 环境,这套工具就废了。反过来,插件全部用 shell 也不现实,逻辑一复杂,shell 脚本根本没法维护。
我最终的做法是框架用 Node.js,但插件不绑定语言——每插件声明一个解释器,框架只负责用 spawn 把子进程拉起来。这样写 JSON 解析的插件可以用 Python,写 Git 操作的插件可以用 Go,写文件批处理的直接用 bash。框架不管插件内部怎么实现,只管把参数传对、把 stdout/stderr 收回来、把退出码透传。这个设计在开发插件时带来了不小的灵活性。
2.2 目录结构与命令路由设计
CLI-Anything 的命令路由设计借鉴了 Git 的子命令体系,但增加了一层分组:
anything <group> <command> [options]比如anything json pretty表示"JSON 组下的 pretty 命令",anything net port --pid表示"网络组下查看端口占用的命令"。
这样做的好处是:命令不是摊平的一张大表,而是按领域分组,语义清晰、可发现性强。每个插件目录名就是 group 名,插件内部可以定义多个 command。
路由查找逻辑是从group+command两级精确匹配到插件注册信息,然后由统一参数解析器解析--key value或--flag形式的参数,注入环境变量或者作为 stdin 传给插件脚本,最后等待子进程退出,按退出码输出结果或报错信息。
2.3 配置管理的设计思路
配置这块我踩过几个坑,简单说一下最终方案。CLI-Anything 支持三层配置:默认配置(框架内置)→ 项目本地配置(当前目录下.anythingrc)→ 用户全局配置(~/.anything/config.json),优先级逐级覆盖。配置项包括插件目录位置、超时时间、默认输出格式、常用参数别名等。
为什么不做成单一配置文件?因为不同场景需求差异很大——个人使用的别名是私有的,团队项目里的参数默认值跟着仓库走,内置默认值则是兜底,保证开箱即用。如果只有一层的全局配置,要么个人配置容易被项目覆盖,要么项目需求无从下发,三层结构是这方面比较稳妥的经验。
3. 实操过程与核心环节实现
3.1 安装与初始化
安装的过程比较常规。核心命令是初始化——拉取框架后,第一次运行anything init会创建相关的配置目录结构,并给出一个交互式引导。引导包括:选择插件存放位置(默认在 home 目录下)、是否启用自动补全、是否生成示例插件。
装完之后,最好把自动补全配置加进 shell 的 rc 文件里。这一步很多人容易忽略,实际体验差异很大——没有补全,记命令全靠背诵;有了补全,anything json p<Tab>就能自动补全到pretty。
下面是初始化完成后的骨架结构:
~/.anything/ ├── config.json ├── plugins/ │ ├── json/ │ │ ├── plugin.json │ │ └── impl.py │ ├── net/ │ │ ├── plugin.json │ │ └── impl.sh │ └── file/ │ ├── plugin.json │ └── impl.js └── logs/3.2 核心命令的开发流程:以 JSON 工具组为例
拿 JSON 组作为一个完整的开发示例。JSON 处理的痛点通常有两个:格式化("这个接口返回的 JSON 是什么结构我一团乱麻")和筛选("这个嵌套结构里我要的那几个字段值是什么")。
插件的plugin.json里声明了命令信息:
{ "group": "json", "command": "pretty", "description": "格式化 JSON 字符串或文件并高亮输出", "usage": "anything json pretty [--indent 2] [--input file.json]", "interpreter": "python3", "args": [ { "name": "indent", "type": "int", "default": 2, "desc": "缩进空格数" }, { "name": "input", "type": "path", "default": null, "desc": "输入文件路径,缺省读 stdin" } ] }实现文件impl.py里是真正干活的逻辑:
#!/usr/bin/env python3 import json import sys import os from pathlib import Path def main(): indent = int(os.environ.get("ANYTHING_ARG_INDENT", "2")) input_path = os.environ.get("ANYTHING_ARG_INPUT") source = "" if input_path: source = Path(input_path).read_text(encoding="utf-8") else: source = sys.stdin.read() try: data = json.loads(source) print(json.dumps(data, indent=indent, ensure_ascii=False)) sys.exit(0) except json.JSONDecodeError as e: print(f"[json/pretty] 解析失败: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()实现逻辑并不复杂。需要注意设计细节:框架不直接通过命令行参数传值,而是把解析好的参数写入环境变量,插件从环境变量里读取。这样做的好处是避免不同语言对引号转义的理解差异——反正都走进程环境变量,用什么语言都不会被参数注入坑到。
管道支持也是这个阶段就考虑进去的。curl ... | anything json pretty这种用法必须顺畅,所以默认从 stdin 读,有--input时读文件,两条路都通。
3.3 参数解析与环境变量注入机制
CLI-Anything 的参数解析采用双重标准:--indent 2这种直觉参数风格是主要的,同时支持声明短参数别名(例如-i对应--indent),解析完成之后,框架会把所有参数都注入到插件进程里。
注入命名规则是ANYTHING_ARG_前缀加上大写参数名。例如参数名input对应环境变量ANYTHING_ARG_INPUT;对于--no-highlight这类布尔开关,注入的值为"true"或"false"。
这套约定带来的统一性很有价值:所有插件的感知方式都一样,不需要每个插件自己写一套 getopt 或者 argparse 来解析参数——解析这件事交给框架做,插件只从环境变量里取值即可。团队内部新增插件的人只需要理解"参数会被注入成ANYTHING_ARG_XXX环境变量"这一个规则。
如果参数类型声明为path,框架还会做路径标准化,把相对路径转换成绝对路径后再注入,避免插件执行时因为工作目录不同产生问题。这个细节看起来不起眼,实际使用中能避免一批"在 A 目录下正常、在 B 目录下就找不到文件"的诡异问题。
3.4 网络与进程排查类命令的实现逻辑
JSON 类插件的逻辑偏向数据处理,网络类插件则偏向系统能力调用。我写了一个net port命令,作用是为当前机器的端口找到对应的进程信息。
底层实现采用了分段策略:优先用lsof -i :PORT(如果存在),否则退回netstat -ano(Windows 风格)或/proc/net/tcp解析(Linux 无 lsof 环境)。框架层面的命令还是同一个anything net port --port 8080,不同系统走到不同的底层逻辑。这是插件式架构的典型优势——把平台差异隔离在子进程内部,使用者面向统一的命令接口。
impl.sh的关键部分:
#!/usr/bin/env bash set -euo pipefail PORT="${ANYTHING_ARG_PORT:-}" if [ -z "$PORT" ]; then echo "[net/port] 请通过 --port 指定端口" >&2 exit 1 fi if command -v lsof >/dev/null 2>&1; then lsof -i :"$PORT" elif command -v netstat >/dev/null 2>&1; then netstat -ano | grep ":$PORT" || echo "端口 $PORT 未被占用" else echo "[net/port] 当前系统没有 lsof 或 netstat,无法查询" >&2 exit 2 fi类似的还有file组下做批量重命名:支持--pattern和--replacement参数,实现上就用 shell 的rename或者循环加mv。注意这种批量操作为什么要放在统一 CLI 里?因为批量和 glob 匹配有很高的误操作风险,把"先 dry-run 展示将发生的修改、再加--apply真正执行"的安全机制做进默认流程,要比每次手写一个for循环安全得多。这个设计思路源于我考察 kubectl 等成熟 CLI 的经验——破坏性操作必须可视化再加确认。
3.5 输出风格与错误处理规范
CLI 工具的输出风格决定了它好不好用。早期我做过一个纯 dump 输出的版本,所有插件结果直接 stdout 一丢,结果就是脚本解析友好、人眼阅读吃力。后来分析了一些优秀开源 CLI 的设计,总结了这套输出规范:
框架提供统一的输出封装。正常结果走 stdout,保持纯文本(方便管道传递);错误信息走 stderr,统一带[group/command]前缀,方便日志检索;退出码遵循惯例——0 表示成功、1 表示逻辑性失败(如 JSON 解析失败)、2 表示参数错误、其他码留给插件自定义。
这样一套规范落实到所有插件后,CLI-Anything 的输出就稳定了:在脚本里调用它,错误信息不会被混进管道数据里,对排错非常友好。
4. 扩展开发与自定义工具接入
4.1 三分钟接一个团队内部脚本
CLI-Anything 定位成团队工具的"收纳盒"以后,我列出了内部常见脚本的接入需求:日志归档脚本、代码生成脚本、数据库巡检脚本。这些脚本大多是之前同事用不同语言零散写出来的,接进统一 CLI 其实不需要重写。
步骤如下:在plugins目录下新建一个子目录,目录名就是分组名,比如ops;在里面放plugin.json,声明命令名和解释器;再放一个入口文件,内容要么是脚本本身,要么是原来脚本的软链接。最后跑一下anything list确认注册生效。
整个过程不用动框架代码。这一点是插件式设计最直接的收益——接入成本足够低,团队才愿意长期用下去。
4.2 插件开发规范与命名约定
插件开发规范里,有几个约定值得参考:
插件目录名和plugin.json里的group必须一致,不一致会导致找不到路由。命令名必须在 group 内唯一,否者启动时报错。解释器字段(interpreter)必须写绝对路径或相对 PATH 的可执行名——建议路径里有空格时用引号包裹。插件脚本头部必须写 shebang。stdout 只输出正常数据;所有调试信息、警告、异常提示统一走 stderr。
这个规范的弹性在于:尽量少限制插件内部怎么写,只约束外壳边界——命令名唯一、输出分清流、退出码有语义。团队内部接手的人只要记住了这三条,就不太会写出混乱的插件。
4.3 内置工具清单与规划
目前内置的工具组包括:
json组:格式化、校验、提取。提取操作可以用 JSONPath 语法从嵌套结构里取值。net组:端口查询、HTTP 请求。HTTP 请求统一支持--method、--header、--body,比直接拼 curl 参数好记。file组:批量重命名、重复文件查找、文件类型统计。text组:字符串替换、Base64 编解码、URL 编解码。time组:时间戳转换、日期计算。
实际使用中,json get --path '$.user[0].name' --input user.json这类命令用起来很有"统一入口"的感觉—— 不用记 jq 语法、不用开一个 Node 交互窗口、不用写 Python 脚本,一行命令解决问题。
4.4 自动补全与帮助系统的落地细节
CLI 工具没有补全,学习成本会显著上升。CLI-Anything 的补全实现不复杂——框架维护了一份命令树,按group→command→args展开,生成 shell 补全脚本,用户执行anything completion install后由框架追加到对应 rc 文件。
帮助系统是我后来迭代收益很大的一个改动。命令根级别执行anything不带参数时,不是打一行使用说明就完,而是输出分组菜单,列出每个组下的命令。执行anything <group> --help时,显示组内所有命令及其用途。执行anything <group> <command> --help时,显示该命令的参数表、示例、别名。三层递进的帮助结构,让新用户即使完全没看过文档,也可以从第一层逐级探索到底。
5. 常见问题与排查技巧实录
5.1 插件找不到或者命令不生效
典型表现:anything list能看到插件,但执行时提示命令不存在;或者刚添加的插件一直不出来。
排查路径:先跑anything debug --plugin <group>查看框架加载这个插件时的完整日志。一般原因是plugin.json里command名与目录名不一致、plugin.json是非法 JSON、或者插件解释器路径在环境中不可用。
这里有个很容易掉进去的坑:plugin.json文件必须放在插件目录的最外层,不能放嵌套子目录里,否则扫描程序不会递归识别它。早期为了演示"一个插件可以包含多个命令"这种组织方式,我试图把多个实现文件放在子目录里,结果扫描时全部漏掉了。
5.2 中文路径、空格路径与特殊字符处理
跨平台 CLI 最大的坑之一就是路径里有空格或中文字符。框架在解析path类型参数时统一做了一次规范化处理。但是插件自己在 shell 里接收路径变量时,一定要在引用变量处加双引号——"$ANYTHING_ARG_INPUT"而不是$ANYTHING_ARG_INPUT。不要问我为什么反复强调,我亲眼见过有同事脚本里不加引号导致所有带空格文件名的文件操作全部失败。
另外,Windows 环境下要特别小心编码问题。CMD 和 PowerShell 默认编码不一致时,环境变量里的中文路径可能变成乱码。目前规避手段是:在插件脚本第一行强制执行set -u和显式设置 UTF-8 环境变量;遇到顽固乱码时,统一走--input传递真实路径而不是依赖当前工作目录。
5.3 管道环境下 stdout 与 stderr 的混用问题
比较隐蔽的问题是管道环境下 stdout 被插件自己的日志输出污染。以 JSON 格式化为例,不少语言的日志库默认把日志写到 stdout 上,这会导致anything json pretty | jq .这类用法失败——因为 jq 拿到的不只是 JSON,还混了日志行。
行业里的普遍做法是插件内统一把日志写到 stderr。CLI-Anything 框架层级也做了一层兜底:如果 stdout 的内容不是合法 JSON 但插件声明的输出类型是 json,框架给出警告而不是直接丢弃。这至少让"格式错误"变得可发现。
踩过几次坑之后我总结出一个经验:插件代码里 print 的每一行都可能成为管道数据的一部分,所以在面向管道使用的命令里,print 必须克制。
5.4 权限与安全边界
插件式 CLI 框架天然涉及安全边界问题——插件本质上是可执行代码。CLI-Anything 在默认配置里设了几个边界:插件默认不要求 root 权限,建议以普通用户执行;--input参数不会读取任意路径以外的文件到内存中二次分发;在执行破坏性操作(如批量重命名、进程 kill)之前,插件默认要求--yes确认。
框架层面还做了第三方插件的"未签名警告":从外部拷贝进来的插件,第一次执行时输出警告提示,并记录插件文件的 SHA256,后续如果文件内容发生变化,再次执行时继续提示。这个机制不算复杂,但对内部团队来说很重要——可以防止有人修改过的插件在无感状态下执行了不同逻辑。
5.5 各问题速查表
把上面说过的问题整理成一个速查表,方便真出问题时快速对照:
| 症状 | 可能原因 | 推荐排查动作 |
|---|---|---|
| 插件命令提示不存在 | group与目录名不一致 | 跑anything debug --plugin <group> |
| 新插件迟迟不出现 | 插件放在子目录而非顶层 | 检查目录嵌套层级 |
| 中文路径乱码 | 编码不一致 | 显式设置 UTF-8 环境变量 |
| stdout 混入日志导致管道坏掉 | 插件 print 了调试信息 | 改走 stderr,管道另测 |
| 参数带空格解析成了多段 | 配置未声明为 path 类型 | 在 plugin.json 中指定 type 为 path |
| 批量操作误伤文件 | 缺少确认机制 | 插件必须实现--dry-run+--apply |
6. 下一步演进方向:从工具集到能力平台
6.1 自动补全与交互式模式增强
当前版本已经支持基础的自动补全,但演进的方向是让补全理解每个参数的取值类型。例如--port参数可以自动列举当前系统正在监听的端口号,--input参数可以基于路径前缀自动列举文件。这个方向的价值更多在于降低记忆成本—— 使用者不需要记住端口号或完整路径,补全会帮他完成一部分记忆工作。
交互式模式也在规划里:执行anything shell进入 REPL 环境后,支持从上一次命令结果中提取参数、保存常用命令片段、条件执行后续步骤。这相当于把统一 CLI 从"单命令执行器"进化成"终端工作流会话"。
6.2 与 LLM 结合的自然语言 CLI 界面
在我个人规划里,这项是 CLI-Anything 最有潜力的方向。既然所有命令都是插件声明、参数都是结构化配置,那么在plugin.json一览齐全的情况下,LLM 完全可以充当翻译层——用户用自然语言描述意图("把当前目录下所有 .tmp 文件移到 /tmp/archive 下面"),LLM 解析后生成对应的anything file move --pattern '*.tmp' --dest /tmp/archive命令,再由框架执行。
这个方向为什么有价值?因为 CLI 的"确定性"和"可解释性"仍然保留,而"学习成本"却可以被自然语言大幅拉低。对于团队内部那些不常用、容易忘参数的命令,自然语言入口几乎是刚需。
6.3 团队级插件仓库与一键分发
最后这个方向偏基础设施——团队内部建立一个插件仓库,anything install <插件名>从公司内部的 Git 仓库一键安装、anything update --all批量更新。分发机制的重点不在于"下载文件"这一步,而在于版本锁定和变更审计——谁在哪个时间点装了什么版本,必须可追溯。
对团队来说,这套机制能把散落在各处的脚本变成一套有版本的、可回溯的工具资产。
最后的经验之谈
按惯例分享一段个人体会。CLI-Anything 这类统一命令行入口,最大的价值不在技术层面,而在日复一日的使用惯性上——当你知道"任何终端琐事都能从同一个命令入口解决"时,你对终端这个环境的信任感会明显提高。我见过不少开发者技术上很熟练,但日常操作还是习惯去翻历史命令、去找之前贴的笔记,这其实就是工具碎片化带来的内耗。
另外一个小建议:刚开始给 CLI-Anything 接插件时,先接自己使用频率最高的三个操作,不要贪多。把最常用的几条命令打磨顺,形成使用惯性,之后再加插件就会很自然。如果一开始就试图把所有操作全部迁移过来,反而会因为"移植成本 > 学习收益"而放弃整个框架,那才是可惜的事情。
命令行这条路,门槛高,但一旦把高频入口理顺,效率提升是立竿见影的。希望这份拆解能让你少踩几个坑。