
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。路由查找逻辑是从groupcommand两级精确匹配到插件注册信息然后由统一参数解析器解析--key value或--flag形式的参数注入环境变量或者作为 stdin 传给插件脚本最后等待子进程退出按退出码输出结果或报错信息。2.3 配置管理的设计思路配置这块我踩过几个坑简单说一下最终方案。CLI-Anything 支持三层配置默认配置框架内置→ 项目本地配置当前目录下.anythingrc→ 用户全局配置~/.anything/config.json优先级逐级覆盖。配置项包括插件目录位置、超时时间、默认输出格式、常用参数别名等。为什么不做成单一配置文件因为不同场景需求差异很大——个人使用的别名是私有的团队项目里的参数默认值跟着仓库走内置默认值则是兜底保证开箱即用。如果只有一层的全局配置要么个人配置容易被项目覆盖要么项目需求无从下发三层结构是这方面比较稳妥的经验。3. 实操过程与核心环节实现3.1 安装与初始化安装的过程比较常规。核心命令是初始化——拉取框架后第一次运行anything init会创建相关的配置目录结构并给出一个交互式引导。引导包括选择插件存放位置默认在 home 目录下、是否启用自动补全、是否生成示例插件。装完之后最好把自动补全配置加进 shell 的 rc 文件里。这一步很多人容易忽略实际体验差异很大——没有补全记命令全靠背诵有了补全anything json pTab就能自动补全到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(encodingutf-8) else: source sys.stdin.read() try: data json.loads(source) print(json.dumps(data, indentindent, ensure_asciiFalse)) sys.exit(0) except json.JSONDecodeError as e: print(f[json/pretty] 解析失败: {e}, filesys.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 -anoWindows 风格或/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 21; then lsof -i :$PORT elif command -v netstat /dev/null 21; 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--apply6. 下一步演进方向从工具集到能力平台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 接插件时先接自己使用频率最高的三个操作不要贪多。把最常用的几条命令打磨顺形成使用惯性之后再加插件就会很自然。如果一开始就试图把所有操作全部迁移过来反而会因为移植成本 学习收益而放弃整个框架那才是可惜的事情。命令行这条路门槛高但一旦把高频入口理顺效率提升是立竿见影的。希望这份拆解能让你少踩几个坑。