
1. 从“工具太多”到“一个终端”CLI-Anything 的诞生背景前阵子同事问我一个问题你每天在浏览器、终端、文件管理器、云控制台之间来回切不累吗我当时愣了一下因为我确实没觉得累我只是习惯了。但那天回家我做了个实验统计自己一天到底切换了多少次窗口——结果超过三百次。就是从那一刻起我动了做 CLI-Anything 的念头。1.1 天天切窗口的人到底在切什么仔细分析了一下切换窗口背后的操作其实非常低级打开某个网站、查一条日志、改一个配置、发一个请求、整理几个文件。这些事情单独看都不难真正烦人的是“上下文切换”本身。你在编辑器里写着代码突然要切到浏览器看接口文档切到终端跑命令切回编辑器继续写注意力早就碎成渣了。我最初的想法很简单能不能把这些重复操作全部收敛到一个命令行工具里让我不用离开终端就完成大部分日常事务不是把浏览器干掉而是把“需要打开浏览器才能做的事”变成“终端里的一句话”。CLI-Anything 这个名字就是那时候起的——它的野心就是用一条命令去接管“Anything”。1.2 为什么现成的脚本和小工具都不够用在开始写之前我其实调研过一堆方案有人用一堆 shell alias有人用 AutoHotkey 模拟按键有人用 Redis 脚本搭个人控制台。它们都能解决某个具体痛点但都有一个共同问题——太散了。别名脚本越攒越多最后自己都记不住模拟按键只能绑定固定的图形界面位置窗口一调大小就失效自建控制台又要维护一套单独的体系成本比收益还高。我想要的不是“另一个效率工具”而是一个统一的接入层任何工具、任何服务、任何数据源都可以通过一个标准协议挂进来然后暴露成命令行子命令。这正是 CLI-Anything 的定位。1.3 适合谁用能解决什么问题如果你是一个每天要跟大量系统打交道的开发者、运维、数据分析师或者纯粹是受不了鼠标点来点去的效率强迫症这个项目应该对你有吸引力。它解决的是一个非常朴素的问题把高频、重复、确定性的操作从“图形界面里的多个步骤”压缩成“终端里的一句话”并且让这些操作可以被记录、被复用、被分享。后面我会先把核心架构讲清楚然后给出可以直接抄的代码实现再带大家跑一遍真实场景最后说说我在过程中踩过的坑和如何扩展自己的插件。重点不是让你照搬我的项目而是让你理解“把 anything 变成命令行”这件事的设计思路。2. 核心架构设计让任何东西都“长成”一行命令单看“CLI-Anything”这个名字容易把它理解成一个什么都能干的超级命令。实际上它本身不实现任何业务功能它只是一个壳、一个协议、一套调度机制。具体能做什么完全取决于你挂了哪些插件。这个理念我从一开始就定死了核心保持轻量能力全部外置。2.1 一切皆插件命令就是插件插件就是命令CLI-Anything 的架构可以用一句话概括每一个顶层命令对应一个插件每一个插件对外暴露若干个子命令。比如ca browser是浏览器插件ca file是文件系统插件ca http是网络请求插件ca sys是系统信息插件。如果你不喜欢默认的ca前缀完全可以在配置里改成任何别名。每个插件在启动时向核心注册一张“命令表”声明自己支持哪些子命令、接收什么参数、返回什么类型的数据。核心不关心插件内部怎么实现只负责把用户的输入路由到正确的插件然后把结果统一格式化输出。这种插拔式设计带来两个直接好处第一核心代码几乎不需要变动新增功能就是新增一个目录第二用户完全可以只安装自己需要的插件不想要的部分直接不加载。2.2 命令路由与命名空间从“一句话”到“精准定位”命令行工具的难点从来不是执行命令而是把用户的输入准确解析成语义。我见过不少工具把所有功能塞进一个大函数里靠长长的 switch-case 分发最后维护成本高到爆炸。CLI-Anything 采用了两级路由第一级根据第一个参数定位插件第二级根据第二个参数定位该插件内部的子命令。一个实际的例子ca browser list --tab这里browser是插件名list是子命令--tab是过滤条件。路由层在解析完第一个参数后会把剩下的参数原封不动交给对应插件去处理。这样做的好处是命名空间隔离——每个插件不需要关心别的插件定义了哪些参数全局参数比如--json、--quiet由路由层统一拦截插件只需要处理自己的业务参数。2.3 统一输出格式人看和机器看两手都要硬命令行工具的输出是最容易被忽视、也最影响体验的部分。很多时候我们写个脚本print 一下就觉得完了结果真正做到生产级才发现问题控制台输出可能被 ANSI 颜色污染管道传输需要纯文本程序对接需要 JSON人眼阅读需要表格。CLI-Anything 在设计时把输出分成了三层。默认情况下输出经过优化的表格文本方便人看加上--json参数后输出严格的 JSON方便程序对接加上--tsv参数后输出制表符分隔的纯文本方便 awk、jq 这些经典管道工具继续处理。核心会为每个插件提供一个统一的输出器插件只需要返回结构化数据由输出器来负责渲染不需要每个插件自己拼字符串。2.4 交互式会话与管道兼容既要任性又要克己还有一个问题让我纠结了很久到底要不要做交互式界面做的话项目会复杂很多不做的话很多操作比如批量选择文件体验很差。我最终的做法是两种模式并存默认支持无交互模式适合脚本和管道调用同时提供ca shell进入一个交互式解释器支持命令记忆、补全和简单的状态保持。这两种模式共用一个核心只是在交互式模式下多了历史记录和实时补全。为了保证管道兼容性交互式模式下输出永远走标准输出而且不会出现任何不经过字符串转义的 ANSI 控制序列。这一点我在后面踩坑部分会再展开因为它在实际使用中非常容易翻车。3. 从零实现命令注册、插件加载到执行器附核心代码很多人在设计 CLI 工具时喜欢直接堆代码写着写着就成了一团乱麻。我建议反过来先把协议定清楚再写实现。CLI-Anything 的核心协议只有三个动作注册、解析、执行。所有一切都围绕这三个动作展开。3.1 插件协议一个 Python 类搞定一切我用 Python 写核心主要看重它的快速迭代和丰富的标准库。每个插件的协议非常简单class BasePlugin: name: str plugin_name description: str def commands(self) - dict: 返回子命令名到处理函数的映射 return {}接口很简单核心只要求插件回答两个问题你叫什么你能处理哪些子命令。至于插件内部是否用了数据库、是否调用外部 API、是否启用了多线程核心一概不关心。这种最小化的约定让插件开发的门槛降到了极低也为后面社区贡献插件提供了便利。3.2 命令注册表启动时自动发现运行时动态挂载CLI-Anything 会扫描插件目录下所有满足命名规则的文件自动导入并实例化。为了避免“隐式导入”导致的问题每个插件文件必须在模块底部声明__all__ [Plugin]并且类名统一为Plugin。这个约定虽然看起来刻意但能有效防止大项目里同名类相互污染的问题。注册表内部就是两个字典plugin_registry存插件元信息command_registry存“插件名.子命令”到处理函数的映射。这样做的好处是路由查找时间恒定而且在补全时可以直接遍历command_registry的键生成所有可能的命令提示。3.3 参数解析核心只做减法插件再做加法一个常见误区是把参数解析做到核心层试图用一套规则覆盖所有插件的需求。实际上不同插件的参数形态差异非常大比如浏览器插件可能需要一个--tab布尔开关网络请求插件可能需要--url、--method、--data这么一长串。CLI-Anything 的做法是核心调用 Python 标准库argparse解析出插件名和子命令然后调用该子命令对应的处理函数把剩余参数以列表形式传过去。def dispatch(args: list[str]): if not args: print_help() return plugin_name args[0] plugin plugin_registry.get(plugin_name) if not plugin: print_err(funknown plugin: {plugin_name}) return if len(args) 2: plugin.show_help() return cmd args[1] handler command_registry.get(f{plugin_name}.{cmd}) if not handler: print_err(funknown command: {plugin_name} {cmd}) return result handler(args[2:]) render(result, args)每个插件在自己熟悉的领域里做参数解析既能利用 argparse 的强制位参数、可选参数、互斥参数等能力又不会把复杂度传导到核心层。这是我整个设计里最满意的一个决定。3.4 执行器与输出渲染数据结构化渲染才简单插件返回什么我定的协议是插件返回一个统一的数据包包含type和data两个字段。type决定渲染器用哪种模板data是渲染所需的原始数据。比如type为table时data是一个列表每个元素是一条记录type为kv时data是一个键值对字典type为message时data就是一段文本。这样做的好处是插件开发者在绝大多数情况下只需要返回“字典”“列表”“字符串”这种最朴素的类型不需要关心终端宽度、对齐、颜色这些琐事。渲染器收到数据包后根据全局参数决定输出表格、JSON 还是 TSV。这也意味着如果你想换成别的渲染方式比如输出 HTML 报告只需要新增一个渲染器完全不用改插件。3.5 Executor 核心代码实操从输入到输出的完整链路下面我给出一段现场可运行的执行器核心代码它涵盖了从参数清洗、插件路由到结果渲染的完整链路省略的只是渲染器内部的具体实现import argparse, json, sys def build_parser(): parser argparse.ArgumentParser(progca) parser.add_argument(plugin, nargs?) parser.add_argument(command, nargs?) parser.add_argument(rest, nargsargparse.REMAINDER) parser.add_argument(--json, actionstore_true, destas_json) parser.add_argument(--quiet, actionstore_true, destquiet) return parser def execute(raw_args, registry): args build_parser().parse_args(raw_args) if not args.plugin: print(ca: a unified command line for everything) return 0 plugin registry.get_plugin(args.plugin) if not plugin: print(fca: unknown plugin: {args.plugin}, filesys.stderr) return 1 if not args.command: plugin.show_help() return 0 handler registry.get_handler(args.plugin, args.command) if not handler: print(fca: unknown command: {args.plugin} {args.command}, filesys.stderr) return 1 payload handler(args.rest) if args.as_json: print(json.dumps(payload[data], ensure_asciiFalse, indent2)) elif not args.quiet: render_table(payload[data]) return 0这套执行器的逻辑非常直白好处是核心层几乎不需要改动。我给这个项目定了一条规矩任何新增功能都不允许修改execute这个函数否则就说明抽象出了问题。运行半年多下来这个约束非常有效核心代码的 bug 率低到可以忽略。4. 三个高频场景实测浏览器、文件系统、网络请求理论说了一大堆下面来点实际的。我把 CLI-Anything 日常用得最多的三个插件拆开来看每个都是真实场景每个都有可以直接抄的配置和命令。4.1 浏览器控制把“开网页”变成ca browser open很多人一听“用命令行控制浏览器”就觉得是要做爬虫或者自动化测试其实最常用的反而是琐碎操作。我用得非常频繁的场景是在终端里聚合查看所有标签页而不是一个个窗口找。插件通过 Chrome DevTools Protocol 和本地调试端口通信无需外部依赖。先启动浏览器并开放调试端口chrome --remote-debugging-port9222然后在 CLI-Anything 里注册一个别名ca browser open https://example.com --new-tab ca browser list --visible ca browser close --like github实测下来ca browser list的输出格式非常像任务管理器每一行是标签的 title、URL 和内存占用。有一天我发现 Firefox、Chrome、Edge 三兄弟各开了三十多个标签页直接用ca browser close --like 60一键清掉了一批过期的后台页。这种操作的爽感是鼠标点半天给不了的。4.2 文件系统批量操作从“右键菜单”到一行命令文件管理器的右键菜单看起来很友好但一旦遇到“把某个目录下所有大于 500MB 的日志文件归档”这种条件操作就彻底露馅了。CLI-Anything 的文件插件的设计理念是不重复造 find、ls、cp 这些轮子而是把它们组合成高频操作模板。ca file find --path /var/log --size 500M --mtime 7d ca file archive --path /var/log --match *.log --dest /backup ca file trash --path ~/Downloads --dry-run--dry-run是一个我非常坚持的参数。任何批量修改类命令都必须先输出“将要执行的操作列表”等你加上--commit之后才真正执行。这里有个经验默认安全在用户主动确认后才执行。这个设计能救很多手误我自己就被救过不止一次。4.3 网络请求与数据抓取临时接口调试的救命稻草以前调试一个接口我的路径是打开 Postman、找项目、找集合、找环境、填参数、发请求、看响应。后来固定为curl加一段 bash但每个接口都得现写参数。CLI-Anything 的 HTTP 插件把最常用的请求封成了声明式配置ca http req --name get_user --id 12345 ca http req --name create_issue --data {title: bug}每个请求的 URL、方法、请求头都提前放在一个 YAML 配置里命令行只需要传变量即可。对于临时测试也可以直接写成ca http get https://api.example.com/ping --timeout 5。我做了个简单对比同一个接口的调试流程耗时大概从 40 秒降到了 8 秒左右。主要省下的不是输入时间而是“想起来去打开 Postman”的心理摩擦力。这个参数填到终端里本身就是一步到位。4.4 实测效果汇总下面这张表是我自己在日常工作中记录的对比数据不同机器和网络环境下会有差异但趋势是一致的场景常规操作耗时CLI-Anything 耗时主要收益聚合查看 80 个标签页3 分钟反复切换3 秒一条命令无需切换上下文批量清理过期文件15 分钟手动挑选20 秒dry-runcommit可审计、可回滚重复调试一个 API40 秒8 秒无需打开 GUI 工具检索一段历史命令10 分钟翻终端记录2 秒插件内置索引跨会话搜索这些数据本身不惊人惊人的是它们汇合在一起的效果我每天无效的上下文切换次数至少少了三分之一。省下来的时间未必立刻体现在任务量上但疲劳感和烦躁感真的是肉眼可见地下降。5. 安全边界与二次确认CLI 越强大越要守规矩一个能在终端里控制浏览器、删除文件、访问网络的工具本质上是把一把万能钥匙交给了自动化。能力越大责任越大安全意识必须从架构层面落地而不是靠使用者自觉。5.1 危险命令分类与默认策略我把插件里所有命令分成了三类只读型、改写型、高危型。只读型命令比如查看文件、列出标签页不需要确认改写型命令比如移动文件、修改配置默认输出操作预览高危型命令比如删除、批量关闭、向外部写数据必须走二次确认流程。类型例子默认行为只读ca file find直接执行输出改写ca file move输出预览带--commit后执行高危ca browser close --like all强制二次确认y/N这个分类不是写死的每个插件可以在注册命令时给handler打上danger_level标签。理论上你完全可以把删除操作标成只读但这样做的后果你自己承担。我把默认级别设得偏保守宁可多一次确认也不要一次误操作。5.2 确认机制设计让人有机会反悔让机器有记录可循二次确认不是简单地打印一句“你确定吗”而是要让人看到“你到底要干什么”。我在确认提示里会带上操作摘要、受影响对象数量、以及实际操作命令的完整字符串。比如ca: will close the following 12 tabs? [y/N] - https://example.com/page/1 - https://example.com/page/2 ...y/N的默认答案是 N也就是说你不输入东西直接回车操作不会执行。这个细节非常重要很多工具默认是 y等于把确认框变成了一个摆设。另外所有高危操作都会写入本地审计日志包含时间、命令、参数、退出码。真出问题了有一条可回溯的链路。5.3 权限沙箱插件可以用什么核心说了算CLI-Anything 引入了简单的权限声明机制。每个插件在注册时声明自己需要哪些权限net网络访问、fs_write文件写、fs_delete文件删除、spawn启动子进程等。用户在首次加载插件时会看到授权提示后续可以在配置文件里调整。plugins: http: allowed: [net] file: allowed: [fs_write, fs_delete] browser: allowed: [net, spawn]如果有人部署了一个恶意插件它不能绕过权限声明去偷偷删文件因为在核心层面就已经拦截了。这层设计的价值在于CLI-Anything 的插件市场以后无论如何发展安全底线都非常清晰。6. 踩坑记录那些文档里不会写的细节项目从原型走到能日常使用中间踩了不少坑。挑几个印象最深的写在这里希望能帮后来人省点时间。6.1 Windows 与 Linux 的换行和编码差异一开始我只在 Linux 上开发所有测试都通过。后来在 Windows 上跑了一下直接崩了。罪魁祸首是编码Windows 控制台默认使用 GBK而我的日志和网络响应全是 UTF-8输出乱成一团。最终的解决方案是统一在渲染层做编码转换并且在启动时检测sys.stdin.encoding将默认编码设为utf-8如果控制台不支持则通过PYTHONIOENCODING环境变量强制指定。这个坑让我明白了一个道理命令行工具的输出编码不能依赖运行环境必须由工具自己强制。尤其是返回 JSON、TSV 时编码不一致会导致管道下游程序解析失败。6.2 异步与并发你以为的“同时执行”其实是阻塞CLI-Anything 的 HTTP 插件早期版本是同步串行请求一次请求响应慢后面所有请求全堵住。改成asyncio.gather并发之后速度有了质的提升但又出现了新问题部分服务器对并发请求有限制有些会直接断开连接。最终的做法是给请求并发数加了限制参数--concurrency默认是 4。不同场景可以手动调整比如批量抓取我一般开到 8调试单个接口时开 1 就够。并发不是越多越好而是要根据目标服务器的承受能力来调。这个教训同样适用于别的工具不要盲目追求“什么都并行”。6.3 终端 UI 刷新交互式模式下的 ANIS 控制序列问题交互式模式的补全和历史菜单涉及大量 ANSI 控制序列。问题在于当你在交互式菜单上按了几个方向键还没有真正执行命令时这段菜单的渲染输出如果被用户通过管道截获会把屏幕控制符混进数据流里。我的解决方案是双重保险。第一交互式模式下所有 UI 渲染只写到 stdout但实际命令结果通过独立的文件描述符fd 3传递从机制上隔离开第二核心在非交互模式下默认禁止任何 ANSI 序列凡是要在管道下游继续处理文本的用户必须显式开启--color参数。6.4 性能优化让工具“秒开”的三个关键手段CLI 工具最怕的不是功能少而是启动慢。一多等两秒用户宁愿去点 GUI。为了把启动时间压到毫秒级我做了三件事插件采用懒加载只有收到对应命令时才 import 模块核心数据结构用内置类型避免启动时加载任何重型依赖配置读取只做一次后续命令执行全部走内存缓存。实测下来只加载核心的命令ca version耗时约 60 毫秒加载了全部插件的ca help约 250 毫秒。这个成绩基本让 CLI-Anything 用起来没有“卡一下”的感觉。记住在命令行世界里快永远是第一体验。7. 扩展一个自己的插件把“Anything”接到你的日常CLI-Anything 的价值不仅在于自带的插件更在于它允许你非常低成本地把自己的日常工具接入同一个入口。这里用一个最简示例演示整个过程。7.1 插件目录结构与注册方式假设我想管理自己的待办事项。只需要创建一个todo.py放在插件目录里from core.base import BasePlugin class Plugin(BasePlugin): name todo description manage todo items def commands(self): return { add: self.add_todo, list: self.list_todo, done: self.done_todo, } def add_todo(self, args): # TODO: 存储逻辑 return {type: message, data: added} def list_todo(self, args): return {type: table, data: []} def done_todo(self, args): return {type: message, data: done}把文件丢进插件目录后CLI-Anything 启动时会自动扫描并加载它不需要改任何核心代码。插件与核心的契约就是前面说的三个方法类名、插件名、命令表。7.2 写一个真正能用的插件还是拿 todo 举例我们把存储落到 SQLite。重点看 add 和 list 这两个函数怎么组织数据层和展示层如何分离import sqlite3, json DB_PATH ~/.ca/todo.db def _init_db(): conn sqlite3.connect(DB_PATH) conn.execute(CREATE TABLE IF NOT EXISTS todo (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT, done INTEGER DEFAULT 0)) conn.commit() return conn def add_todo(args): text .join(args) _init_db().execute(INSERT INTO todo (text) VALUES (?), (text,)) return {type: message, data: ftodo added: {text}} def list_todo(args): rows _init_db().execute(SELECT id, text, done FROM todo ORDER BY id).fetchall() return {type: table, data: [{id: r[0], text: r[1], done: x if r[2] else } for r in rows]}从创建文件到真正能用大概五分钟以内。得益于统一的输出协议list_todo返回一个列表渲染层就自动帮你把表格对齐了你完全不需要碰终端宽度和字符串填充这些琐碎的东西。7.3 发布与分发让别人也能一键安装你的插件插件开发完分享也很简单。CLI-Anything 支持从 Git 地址或本地目录安装插件安装过程会做三件事拉取代码、加载插件清单、向用户展示权限声明。ca plugin install gitgithub.com:someone/ca-todo-plugin.git ca plugin install /path/to/local/plugin一个比较实用的技巧是插件清单里尽量声明真实的权限不要为了省事把权限全开。如果插件只需要读取本地 SQLite就不需要申请网络权限。权限声明越克制用户信任感越强反而容易被更多人采用。7.4 后续方向把“Anything”推进到更远的地方CLI-Anything 目前最让我期待的方向有两个。一个是让插件之间能够互相调用比如ca todo add $(ca http get ...)这种组合模式目前已经可以做到只是缺少流畅的语法糖另一个是更智能的参数补全希望未来输入子命令后插件能根据上下文自动提示可选值而不是只提示命令名。工具做到最后你会发现形式和生态比技巧更重要。CLI-Anything 最核心的价值不是它自身的几个插件而是它提供了一个清晰、安全、易于扩展的接入框架。只要接口合理任何人都能把自己的小脚本、小工具挂进这个统一的入口让每天的重复操作真正变成一句话的事。