
1. 为什么我会动“万物CLI”的念头你有没有过这样的经历好不容易在项目管理工具里找到要创建的任务结果发现创建和审批流程要开三个不同页面或者为了调一个数据先打开数据库客户端再打开接口调试工具再打开日志平台整个过程比手工拼报表还慢。我每天有一半时间在终端里工作另一半时间被各种 GUI 工具打断于是慢慢产生了一个想法能不能让我日常碰到的所有事情都用一行命令解决这个想法推着我做了一个叫 CLI-Anything 的小项目。CLI-Anything 的理念很直白把任意任务描述成统一格式的命令定义然后由它自动生成完整的命令行界面、参数校验、帮助文档和执行入口。你可以把 Git 操作、数据库查询、文件批量处理、HTTP 接口调用、甚至 AI 模型的调用都注册进去然后在终端里用一致的风格操作它们。它解决的核心问题不是“再写一个命令行框架”而是“不再为每个新任务单独写一套 CLI 代码”。如果你属于这几类人这篇分享会比较有用每天要跟大量重复性操作打交道的运维和研发总被内部系统折腾得想砸电脑的效率工具爱好者以及想把团队内部脚本统一成规范入口的团队技术负责人。这文章里没有高深理论只有我做完后复盘下来的设计思路、核心实现、实战用例和一些真正踩过的坑。1.1 从“打开一堆GUI”到“一行命令干完所有事”推动我做这件事的最直接场景是一次数据库报表对比。当时我临时要对比线上环境和预发环境里某张业务表的字段差异常规流程是打开数据库客户端、填连接信息、执行查询、导成 CSV、再写小脚本对比两份文件。如果再来一个环境整个流程再复制一遍。我做完第一次就觉得不对劲这个事情明明可以是一条命令解决的为什么我要在四个工具之间来回倒腾于是我把查询逻辑封装成一个 shell 脚本把各个环境的连接参数放在不同配置块里在终端里用dbq --env staging --sql show columns from orders解决问题。这个体验极大刺激了我只要愿意做一次抽象重复劳动是可以被彻底消灭的。之后我开始观察自己每天的命令历史和浏览器打开频率发现八成重复操作都属于“有明确输入参数、有确定执行逻辑、输出要么是文本要么是文件”的模式。这类操作恰恰是 CLI 最擅长处理的场景。它们不需要漂亮的图形界面需要的是固定入口、可自动化、可组合输出。CLI-Anything 的定位就是从这些重复劳动中提炼出一套通用描述方式让你不必为每个小任务都写一遍参数解析和帮助信息。1.2 CLI-Anything 的定位和设计哲学很多工具在解决重复劳动问题时走的是“提供一堆原子命令”路线比如 Git 本身就有几十个子命令你再包一层也只是换个名字。CLI-Anything 想做的不是命令的集合而是命令的创作方式。它希望你把业务逻辑留在任何你顺手的地方不管是 shell 脚本、Python 函数、HTTP 请求还是某个内部平台然后把“如何调用它”这件事集中管理起来。因此它的第一原则是“声明式命令定义”。每个命令只需要一份 YAML 或 JSON 描述包含参数、默认值、校验规则、帮助文案、执行方式。CLI-Anything 读取这些定义后自动生成用户在终端里看到的界面。你想加一个命令不需要写一行参数解析代码只需要多写一份描述文件。第二原则是“执行逻辑与界面层解耦”。界面层负责接收用户输入、校验参数、格式化输出执行层由适配器负责可以调本地 Shell、跑 Docker、发 HTTP 请求、执行数据库查询。这样做的好处非常明显同一个命令定义既可以在本地终端跑也可以放到远程服务器上用既可以由人输入参数也可以由 CI 系统传入参数自动执行。1.3 适合的能力边界什么任务值得做进 CLICLI-Anything 不是用来取代所有软件的什么事情适合做进去我花了不少时间才摸清边界。适合的任务通常有三个特征有稳定的输入参数、有可被验证的输出结果、不需要复杂的图形交互。比如创建一条 Git 分支并推送远端输入是分支名和基础分支输出是执行结果比如查询某张表的行数输入是环境名和表名输出是数字再比如把一个 Markdown 仓库批量转换成结构化 JSON输入是目录路径输出是 JSON 文件。不适合的任务往往是那些交互很重、状态很多、结果需要可视化呈现的操作比如审阅一份复杂的代码变更、比较两份长文档的细节差异、设计一个页面布局。这类事情强行塞进命令行只会让命令参数爆炸维护成本也高。我一开始恨不得所有事情都做成命令后来发现一个命令超过五六个参数时写文档和记忆成本就开始超过操作收益。所以现在我的习惯是如果一个问题无法用一句简短的话描述清楚输入和输出那就先别做成命令宁可继续用 GUI。2. 不急着写代码CLI-Anything 的边界与抽象决定动手实现之前我先把市面上主流的命令行框架过了一遍。常见方案大致可以按语言分类Node 生态有 Commander.js、YargsPython 生态有 Click、TyperGo 生态有 Cobra。这些框架都很成熟但它们的共同点是“围绕一门语言、一种业务写死命令”你和命令逻辑深度绑定。比如你用 Python 写了一个 Click 命令想把它变成团队里的通用入口通常只能在 Python 项目中复用想接入一个远程 HTTP 接口或已有的 Shell 脚本就得再写一层胶水代码。我真正想要的形态更像一个“命令描述解释器”用一份与语言无关的清单描述命令长什么样用一套适配器去执行任何后端。这和工程里的“界面与实现分离”是同一个思路类比来讲就像你把家电的插头做成统一规格电视机、冰箱、电饭煲各自内部再复杂插头都长一个样。CLI-Anything 就是这个统一插头。2.1 常见 CLI 框架的差异与局限这里列一下我调研过的主要方案方便你做选型时对照。Commander.js 最适合在 Node 项目里快速搭建子命令式 CLI链式 API 写起来很顺手但命令的定义和执行逻辑通常写在同一个文件里换一个业务场景就得重新搭框架Click 是 Python 用户的最爱装饰器写法很简洁参数类型自动转换和 Flask 的集成都做得很好但同样存在“命令与代码深度绑定”的问题我想要的是“一份命令描述随处执行”而不是“每个命令都是用代码定义的”。Go 生态的 Cobra 生成原生二进制非常高效适合做交付型命令行工具比如 kubectl、gh 这类重量级 CLI但它的信息密度高也意味着如果你想在运行时动态加载新命令要为插件机制写不少额外代码。这些框架本身的定位没有错只是不适合我想建立的“命令定义仓库”模式。相比之下CLI-Anything 更像是这些框架之上的一个“描述层”它不在乎你的执行后端是什么它只负责把用户的输入变成一份标准化的调用请求。方案优势局限适合场景Commander.js子命令支持好、生态丰富命令与 Node 代码绑定Node 项目内组件化 CLIClick / Typer参数类型转换强、文档生成好命令定义依赖 Python 代码Python 工具链、ML 项目Cobra性能好、可编译单文件动态加载新命令成本高面向外部用户的重量级 CLICLI-Anything声明式定义、后端无关需要维护适配器协议团队内部统一命令入口2.2 核心抽象命令描述与执行适配器分离CLI-Anything 只定义了两个核心概念命令描述Command Manifest和执行适配器Adapter。命令描述只回答“命令长什么样”包括名字、作用、参数、校验、示例执行适配器只回答“命令怎么跑”包括本地 Shell、远程 HTTP、数据库连接、容器执行等。两者之间靠一个标准化的调用对象沟通这个对象里保存了参数值、环境配置、输出格式要求。这个抽象帮我解决了一个长期头疼的问题团队里有人习惯写 Python有人只会写 Shell还有人希望直接请求内部接口。过去要把这些能力统一到一个 CLI 工具里往往需要选一种语言把所有人拉进来。现在大家只需要各自提供一个可被调用的后端入口然后在 CLI-Anything 里写一段描述命令入口就统一了。即使有人想换实现语言也只是换一个适配器的事用户侧的命令行体验完全不变。2.3 三种主要适配器类型我实现的前几版适配器里最常用的是 ShellAdapter、HttpAdapter 和 PythonAdapter。ShellAdapter 适用于已经存在的大量 bash 脚本它的工作就是把参数插进脚本模板然后用子进程执行。这也是最容易上手的适配器因为几乎任何团队都有存量脚本。HttpAdapter 适合那些已经把逻辑做成了内部服务的场景。命令参数会被拼成请求参数或 JSON body返回结果按配置解析后输出。这个适配器的好处是远程团队只要保证接口稳定别人就可以在终端里用它不需要登录、不需要查接口文档。PythonAdapter 适合更复杂的计算型任务比如批量重命名、文件格式转换、数据清洗直接用 Python 函数作为执行体方便写单元测试。这一阶段的收获是不要一开始就把适配器设计得特别抽象。我第一版试图定义一套支持“任意语言插件”的协议结果光写协议模板就花了一周后来发现一个简单的字典调用约定就能满足绝大多数需求。项目落地时真正有价值的往往是“少定义概念多解决具体问题”。3. 核心实现描述文件驱动 动态命令分发我选 Python 作为第一个实现语言主要是因为团队内的脚本生态以 Python 为主而且 Python 标准库里的 argparse 够用、yaml 解析也方便。整个项目的入口只有一个命令cli-anything。它启动时先扫描指定目录里的所有 manifest 文件把它们定义的命令注册到内存中然后根据用户输入的命令名分发到对应适配器。这里有个关键设计所有命令参数校验都从 manifest 定义自动生成而不是手写。这么做的直接好处是帮助文档和校验逻辑永远跟命令定义保持一致。过去手写 CLI 最容易出现的情况是参数说明早就改了帮助文本还停在旧版本声明式定义从根源上避免了这个问题。3.1 命令描述文件的 Schema 设计我参考了 OpenAPI 的参数描述思路设计了一份尽量精简的 manifest 结构。每个命令有五个核心字段name、description、params、adapter、run。其中 params 定义所有参数每个参数包含类型、是否必填、默认值、帮助文案adapter 指定执行方式run 根据适配器类型不同可以是 shell 脚本模板、HTTP 配置或 Python 函数路径。我实际用下来一个命令描述一般也就 20 到 40 行。以“创建 Git 分支并推送远端”为例version: 1 plugin: git-flow namespace: g commands: - name: newbranch description: 基于指定分支创建新分支并推送到远端 params: - name: branch type: string required: true help: 新分支名称 - name: base type: string default: main help: 基础分支名 adapter: shell run: | git checkout {base} git checkout -b {branch} git push -u origin {branch}这种结构对一个熟悉 YAML 的工程师来说几乎没有学习成本。我把 namespace 设计成命令前缀避免插件之间命名冲突比如g newbranch、dbq run、md build各归各的命名空间。3.2 动态生成 argparse 解析树用户输入cli-anything g newbranch feature/login --base dev流程会是这样启动脚本读取 manifest遍历所有插件命令利用 argparse 动态创建子解析器再交给命令分发器执行。核心代码大致长这样import argparse import yaml from pathlib import Path class CLIAnything: def __init__(self, manifest_dir: Path): self.commands {} for path in Path(manifest_dir).glob(*.yaml): data yaml.safe_load(path.read_text()) for cmd in data.get(commands, []): key f{data[namespace]} {cmd[name]} self.commands[key] cmd def build_parser(self) - argparse.ArgumentParser: parser argparse.ArgumentParser(progcli-anything) subparsers parser.add_subparsers(destplugin_command, requiredTrue) for key, cmd in self.commands.items(): namespace, cmd_name key.split( ) if namespace not in subparsers.choices: ns_parser subparsers.add_parser(namespace) ns_subparsers ns_parser.add_subparsers(destcommand, requiredTrue) else: ns_subparsers ns_parser None cmd_parser ns_subparsers.add_parser(cmd_name, helpcmd.get(description, )) for param in cmd.get(params, []): kwargs {} if param.get(type) integer: kwargs[type] int if param.get(required): kwargs[required] True if default in param: kwargs[default] param[default] cmd_parser.add_argument(f--{param[name]}, **kwargs) return parser def run(self, argv): args self.build_parser().parse_args(argv) self.dispatch(args)这里有一个值得注意的设计子解析器是按 namespace 动态创建的所以对用户来说命令总是“工具 动作”的格式比如g newbranch、dbq run而不是一团散装的子命令。这个习惯一旦养成你就不需要记太多东西看到名字空间就知道它属于哪一类工具。3.3 适配器的实现约定适配器是 CLI-Anything 里最灵活的部分。我把它定义为一个可调用对象接收两个参数命令描述和解析后的参数对象返回标准输出。ShellAdapter 的做法是把run模板里的{param}全部替换成用户输入然后通过 subprocess 执行。import subprocess import tempfile class ShellAdapter: def execute(self, cmd: dict, args: argparse.Namespace) - None: script cmd[run] for param in cmd.get(params, []): value getattr(args, param[name], ) script script.replace(f{{{param[name]}}}, str(value)) with tempfile.NamedTemporaryFile(w, suffix.sh, deleteFalse) as fp: fp.write(script) script_path fp.name subprocess.run([bash, script_path], checkTrue)实际使用时我会在 ShellAdapter 里再区分interactive和silent模式。交互模式会把步骤提示打印到 stderr让用户知道命令执行到哪一步silent 模式只保留最终结果。这个区分很重要因为一旦 CI 系统或其它工具要解析你的输出任何多余的非标准输出都会变成噪音。HttpAdapter 的实现也很简单它把 manifest 里的url、method、params_mapping组合成一个请求然后用 requests 发出。它的额外价值在于参数自动做类型转换整型参数不会变成字符串拼进 URL减少了我日常调接口时最容易踩的坑。3.4 输出格式化默认文本 可切换的 JSON命令执行完之后用户最关心的是结果长什么样。我最初只把适配器的 stdout 原样透传后来发现不同命令的输出风格差距太大有的喜欢打进度条有的只给一个数字有的丢一行 JSON让人难以统一处理。于是我加了一个统一参数--format可取值text和json。这不是什么复杂功能却在团队里产生了很大的用例变化当每个命令都能输出 JSON 时别人就可以用 jq 做二次加工甚至把多个命令用管道组合起来。比如dbq run --sql $(cat query.sql) --format json | jq .count就能直接取出数字根本不用人工盯着表格看。我的经验是给所有命令统一标准输出格式带来的自动化收益远大于实现成本。4. 四个实战任务如何被我塞进命令行概念讲再多不如看实际例子。下面四个任务都不是拍脑袋想的全是我过去几周真实的工作场景。每个我都先描述原来的痛点再贴出 manifest 定义最后讲使用效果和调整思路。4.1 Git 工作流压缩成三条命令团队项目开发通常要经历创建分支、推送远端、提合并请求、部署上线等步骤。我发现自己天天在 Git 命令、代码托管平台网页、部署平台之间来回切换。最先被 CLI-Anything 化的就是这套流程。除了上面提到的newbranch还加了两个命令g commit负责检查变更、生成规范 commit message 并提交g release负责打标签、推远端、触发流水线。这个例子的核心收获是命令名要足够短才能替代旧习惯。一开始我把命令取名create-new-branch-and-push每次敲完都觉得自己在写一句话后来改成newbranch使用率明显提升。团队其他人看我这么用之后也开始把别名写进 shell 配置人终究是懒惰的越短越容易被接受。4.2 数据库查询入口dbq run第二个实用场景发生在数据分析任务上。此前团队成员想要查某个环境的某个表必须先找到连接信息保管密码再打开数据库客户端。中间任何一步出错都会消耗大量时间。我把常用环境信息集中维护在一个配置文件里提供dbq run命令。namespace: dbq commands: - name: run adapter: python run: adapters.mysql_query.execute params: - name: env type: string required: true help: 环境名可选 dev/staging/prod - name: sql type: string required: true help: SQL 语句或者用 file.sql 读取文件 - name: format type: string default: text help: 输出格式支持 text 或 jsonadapters.mysql_query.execute这个 Python 适配器负责读取环境配置、创建连接、执行 SQL、格式化结果。现在团队成员不再需要知道数据库地址和账号只需要知道业务含义是查询哪张表这降低了新人上手门槛也顺带解决了很多安全隐患。4.3 Markdown 仓库批量转成 JSON第三个例子是关于文档处理的。我维护一个技术笔记仓库里面几百篇 Markdown 文件散落着大量信息。要统计每个文档的字数、提取一级标题、检查引用链接是否失效靠人手工做几乎不现实。我给这个仓库加了一个md stat命令参数是目录路径输出各文档的标题树、字数、标签。执行逻辑用 Python 适配器实现内部调用 markdown 解析库把每篇文档转成结构化对象后再统计。这个例子稍微复杂一点但它验证了 CLI-Anything 不只是包装现有脚本也可以承载真正的数据处理逻辑。命令的参数很简洁但后端的逻辑完全可以很复杂这种“入口简单内部复杂”的形态正是我想要的工具形状。4.4 AI 接口做 commit message 推荐最后一个例子是近期才加上的。我发现在写完代码之后每次都要花几秒钟想 commit message虽然单次时间不长但积累下来也很可观。我把命令行工具和 AI 接口接了起来新增一个g suggest-message命令自动收集当前分支的变更 diff截断到合理长度拼装一条提示词请求 AI 接口返回几条候选提交信息。这个任务接入 HTTP 适配器非常顺手因为接口本身就是一个标准的 HTTP 调用。在 manifest 中把diff用git diff --cached动态生成再通过 Python 适配器发请求。实际使用时我会把这个命令的输出推荐在前自己改一个词再提交体验比从零想一个 commit message 顺畅得多。这里想强调的是CLI-Anything 很适合做这类“聚合型”任务它可以把本地命令、外部接口和文本处理组合成一个看起来非常简单的用户入口。5. 真正“好用”的 CLI 需要避开的坑做完基本功能后我发现一个残酷的事实一个工具如果只在功能层面能用用户体验却很差最终还是会被扔进仓库角落。命令行工具的用户体验体现在参数解析、错误提示、输出规范这些细节上。这里分享几个我踩过而且反复踩过的坑。5.1 参数解析规则别自己造早期版本里我为了让命令支持--namevalue和--name value两种写法自己写了一段字符串解析逻辑。结果测试的时候发现遇到特殊字符和引号时行为完全不可预测项目里也没有人愿意补测试用例。后来我彻底放弃自研解析改用标准库 argparse并明确只用--name value形式。这个改进的效果立竿见影参数带空格、带引号、甚至带 Unicode 字符都能正确处理。很多程序员会犯同样的错误觉得参数解析“不过就是 split 一下”但真实输入远比想象中复杂。把参数解析交给成熟框架把精力花在业务逻辑上是 CLI 工具开发的长期主义。5.2 输出格式是命令行的“UI 设计”命令行工具的界面其实就是它的文本输出。很多人写完适配器直接把 stdout 丢给用户导致文本和错误信息混在一起机器没法解析。这个问题我第一次在 CI 系统里复用命令时暴露出来一个命令本来要输出纯 JSON结果 progress 信息混进了 stdout下游解析直接失败。我的处理规则很简单数据输出进 stdout日志和进度信息进 stderr所有命令默认支持--format json。规则一旦统一命令就可以被安全地组合进管道。后来我还加了一条约定帮助信息打印到 stderr这样cmd --help | jq也不会被帮助文本污染。这一条应该适用于几乎所有 CLI 工具。5.3 错误信息要能“被机器理解”普通用户在命令行里看到一段 Python traceback也许还能忍但自动化流程只要检测到非零退出码就停。如果错误信息不提示出错原因、没有错误码脚本就只能卡在那里等人排查。我在 CLI-Anything 里统一了异常处理任何适配器异常都会被捕获转成标准格式错误码 人类可读的信息 建议排查方向。比如数据库连接失败时不是丢一句“连接超时”就完了而是提示“无法连接 envstaging 的数据库请检查访问白名单或网络代理”。用户可以根据提示信息直接行动而不是去翻配置文件和日志。这种做法在团队协作时尤其重要因为别人不一定有和你一样的调试经验。5.4 交互参数和批处理参数要兼容我最初设计命令参数时只考虑了人坐在终端前慢慢敲的场景后来想把命令接入定时任务却发现不少命令缺少非交互式参数。比如部署命令会弹出一个确认提示确定要发布到生产环境吗这个弹窗在有人盯着的时候没问题自动化执行时却直接卡死。解决方式是引入--yes全局参数一旦传入就跳过所有交互确认强制使用默认值。这个坑非常普遍。建议你在设计任何命令时都问自己如果这个命令在无人值守的环境里运行所有会暂停等待人工输入的地方怎么处理补上非交互模式不仅让自动化成为可能也倒逼你去认真定义每个交互步骤的默认值反而让命令更稳健。6. 下一步方向TUI、远程执行与命令共用CLI-Anything 目前的版本已经能覆盖我日常大部分重复性操作但我知道它还有三个方向值得继续探索从命令行到 TUI、从本地到远程、从一个仓库到一套插件生态。这些方向不是空想而是我在实际使用中真实感受到的痒点。6.1 从命令到 TUI同一份描述两套交互命令行的局限在于它没有状态感。你运行了一个查询命令结果出来后想调整条件再查一次你得手动修改参数重新输入。可在图形界面里你只是拖动一个过滤器。为了解决这个问题我在想是否可以让同一份 manifest 描述被一个 TUI 渲染层消费用方向键选择命令、填写表单、查看结果。技术上有现成的基础设施Python 的 textual 和 urwid 都可以实现这种面板式交互。核心优势是命令描述的定义不用改变只需要新增一个交互渲染器CLI-Anything 就能从“一行一个命令”升级成“一个终端里的小控制台”。我目前只在实验分支上跑通了简单表单渲染效果很好后续值得重点投入。6.2 远程执行把命令中心搬到服务端本地有本地的好处但有些任务必须在特定网络环境下执行。比如生产环境的数据库查询、线上服务的告警处理这些命令放在本地既不方便也不安全。一个自然的演进是添加 RemoteAdapter用户输入命令由本地 CLI-Anything 将参数请求发送到服务端服务端执行真正的逻辑再把标准输出返回给终端。这样做可以大大简化本地依赖团队不再需要各自配置一堆连接信息。只要有一台具备正确网络权限的服务器所有成员都能在上面执行受限命令。安全层面我会继续采用“命令定义与权限模型绑定”的思路不同的团队成员只能看到和使用被授权的命名空间。6.3 插件市场让命令定义可以被共享项目做久了自然会诞生大量 manifest 文件。如果这些文件只能在自己电脑里使用价值就有限。更理想的状态是一个共享仓库任何人可以发布自己打磨好的命令定义其他人执行一行安装命令就能使用就像 Homebrew 或 VS Code 插件市场。形式上可以是cli-anything install namespace这样的命令从 GitHub 或内部 GitLab 拉取一个插件仓库同步到本地 manifest 目录同时自动检查适配器依赖是否满足。发布机制至少包含版本管理、依赖声明、冲突检测三块。这个方向一旦落地团队内部就能形成“命令复用”的文化而不是每个人各写一套脚本来做类似的事。我在这条路上走了大概一个月最大的体会是但凡值得做的工具核心都不是技术亮点而是你愿意持续打磨用户细节。命令行工具是否好用很多情况下不是拼功能多少而是拼参数设计、错误提示、输出约定。如果你也在被各种重复操作折磨不妨从你身边最常用的那两三个动作开始用一条命令把它们收编再一点点扩展到更多场景。这个过程本身会给你比工具成品更多的启发。