
1. CLI-Anything 不是又一个命令行包装器它是 CLI 生态的“操作系统级抽象层”你有没有过这种体验在终端里敲git status想顺手把当前分支名复制到剪贴板结果得先git branch --show-current | pbcopymacOS或git branch --show-current | clipWindows——两步操作中间还容易手抖输错又或者写了个 Python 脚本处理日志想把它变成log-analyze --file access.log --top 10这样的命令却卡在 argparse 参数解析、子命令注册、帮助文档自动生成上最后干脆用python analyze.py --file ...将就了更别提那些需要跨平台、带交互式菜单、还要支持自动补全和历史记录的 CLI 工具光是环境兼容性就能耗掉半天。CLI-Anything 就是为解决这类“明明功能很简单但 CLI 包装成本高得离谱”的问题而生的。它不试图替代argparse、click或typer也不提供新的 DSL 语法糖。它的核心定位非常务实让任何可执行程序、任何 Python 函数、甚至一段 Shell 脚本都能在 5 秒内获得一个符合 POSIX 标准、支持子命令、自动补全、跨平台帮助文档、且无需修改原逻辑的 CLI 接口。关键词不是“强大”而是“零侵入”和“即插即用”。它背后没有复杂的 agent 框架也没有所谓“native agent”概念的玄学包装——那只是社区对“能自动适配不同 CLI 风格并智能路由”的一种口语化表达。真正的技术底座是一套精巧的元数据描述协议 动态加载引擎 统一 CLI 路由器。我第一次用它把一个只有三行print(Hello)的 Python 文件变成hello --version命令时反应是“这玩意儿居然真没骗我”。它和 Codex CLI、Claude CLI、Minimax Code CLI 等工具的本质区别在于后者是特定大模型服务的命令行客户端本质是 API 封装器而 CLI-Anything 是一个通用 CLI 构建与分发平台你可以用它封装自己的模型调用脚本也可以封装数据库迁移命令、自动化测试流水线、甚至公司内部的审批流程 CLI。它不绑定任何模型、不依赖任何云服务纯 Python 实现安装即用。这也是为什么在 GitHub 上它的 star 增长曲线和codex cli完全不同——前者是开发者在解决自己真实工作流中的“毛刺”后者是用户在追逐某个新模型的热度。2. 为什么传统 CLI 开发范式正在失效从 argparse 到 CLI-Anything 的必然演进我们来拆解一个真实场景你写了一个用于清理临时文件的 Python 脚本cleanup.py内容如下import os import shutil from pathlib import Path def cleanup_temp(dir_path: str, dry_run: bool False): target Path(dir_path) for item in target.rglob(*.tmp): if dry_run: print(f[DRY] Would remove {item}) else: item.unlink() print(fRemoved {item}) if __name__ __main__: cleanup_temp(/tmp, dry_runTrue)现在你想把它变成一个命令行工具。传统路径有三条2.1 路径一硬编码 argparse最常见也最脆弱import argparse import os import shutil from pathlib import Path def cleanup_temp(dir_path: str, dry_run: bool False): # ... 同上 if __name__ __main__: parser argparse.ArgumentParser(descriptionClean up .tmp files) parser.add_argument(dir, helpDirectory to search in) parser.add_argument(--dry-run, actionstore_true, helpShow what would be deleted) args parser.parse_args() cleanup_temp(args.dir, args.dry_run)问题在哪帮助文档与代码脱节description字符串是独立维护的函数 docstring 里写的说明没人看类型安全缺失args.dir是str但函数签名里dir_path: str的类型提示完全没被利用扩展性差加个--exclude-pattern参数得改三处add_argument、函数签名、函数体跨平台补全缺失argparse不提供bash_completion或zsh补全脚本生成能力得额外引入argcomplete并手动配置子命令噩梦如果后续要加cleanup logs和cleanup cache子命令argparse的嵌套结构会迅速变得难以维护。2.2 路径二拥抱 Click/Typer更现代但仍有框架锁定用 Typer 改写import typer from pathlib import Path app typer.Typer() app.command() def temp( dir: str typer.Argument(..., helpDirectory to search in), dry_run: bool typer.Option(False, --dry-run, helpShow what would be deleted) ): for item in Path(dir).rglob(*.tmp): if dry_run: typer.echo(f[DRY] Would remove {item}) else: item.unlink() typer.echo(fRemoved {item})优势明显自动从类型提示生成参数、内置帮助、支持子命令。但代价是什么强框架依赖你的cleanup.py现在必须import typer且只能通过typer.run()或typer.Typer()启动部署复杂度上升如果这个脚本要打包进 Docker 镜像你得确保typer在基础镜像里无法复用已有逻辑如果你的cleanup_temp函数已经存在于一个大型项目中且被其他模块调用强行改成 Typer 命令会破坏原有调用链补全仍需配置Typer 虽然支持补全但需要用户手动运行typer install-completion且对非主流 shell 支持有限。2.3 路径三CLI-Anything 的“无感注入”模式这才是本文的核心CLI-Anything 的哲学是你的业务逻辑就是 CLI不需要为 CLI 而重构业务逻辑。它通过一个极简的 YAML 描述文件cli.yaml来“声明”如何将现有代码暴露为 CLI# cli.yaml name: cleanup version: 1.0.0 description: Clean up temporary files across directories commands: - name: temp description: Remove .tmp files recursively python: cleanup.py:cleanup_temp # 模块路径:函数名 arguments: - name: dir type: string required: true help: Directory to search in - name: dry_run type: boolean default: false help: Show what would be deleted然后执行pip install cli-anything cli-any init # 生成基础配置 cli-any build # 生成可执行的 cleanup 命令生成的cleanup命令天然支持cleanup temp --help自动生成的 rich help 文档cleanup temp /tmp --dry-run参数自动转换与校验cleanup temp TABzsh/bash 自动补全基于cli.yaml元数据cleanup --version版本号来自cli.yamlcleanup temp --dir /tmp长参数、短参数-d可选由 CLI-Anything 自动生成提示CLI-Anything 的build命令实际会生成一个轻量级的 Python 脚本类似click的EntryPoint它只负责解析命令行、加载cli.yaml、动态导入目标函数并传参。整个过程不修改你的原始cleanup.py也不要求你安装任何额外依赖——你的业务代码保持绝对纯净。这就是演进的必然当 CLI 工具数量指数级增长git,docker,kubectl,poetry,pre-commit,black,ruff...每个都重复造一遍参数解析、帮助生成、补全配置的轮子是一种巨大的工程浪费。CLI-Anything 把这套基础设施下沉为标准让开发者只需专注“做什么”而不是“怎么做成 CLI”。3. CLI-Hub当 CLI-Anything 遇见包管理构建可发现、可复用、可组合的 CLI 生态CLI-Anything 解决了“单个工具如何快速 CLI 化”的问题但另一个更深层的痛点是我写好了cleanup怎么让团队其他人一键安装、更新、发现总不能每次更新都发个.tar.gz让大家pip install -e .吧这就引出了 CLI-Anything 的孪生组件CLI-Hub。CLI-Hub 不是一个中心化应用商店而是一个去中心化的 CLI 注册与分发协议。它的核心思想是每个 CLI 工具的cli.yaml文件本身就是其“软件包描述文件”。你不需要额外写setup.py或pyproject.tomlcli.yaml已经包含了名称、版本、依赖、入口点等全部元信息。3.1 CLI-Hub 的工作流从本地开发到全球分发假设你完成了cleanup工具并希望发布到团队共享仓库。流程如下本地验证cli-any validate # 检查 cli.yaml 语法、函数路径是否可导入、参数定义是否合理 cli-any test # 生成临时 CLI 并运行集成测试例如 cleanup temp --dry-run /tmp注册到私有 Hub如公司内网 GitLabCLI-Hub 协议规定一个“Hub”就是一个 Git 仓库其根目录下有一个index.yaml文件内容类似# index.yaml repositories: - name: internal-tools url: https://gitlab.company.com/cli/internal-tools.git branch: main你的cleanup工具代码推送到https://gitlab.company.com/cli/internal-tools.git的main分支路径为tools/cleanup/其中包含cli.yaml和cleanup.py。客户端安装团队成员只需执行# 首次配置 Hub cli-hub add internal https://gitlab.company.com/cli/internal-tools.git # 安装 cleanup 工具自动拉取最新版 cli-hub install cleanup # 或者直接运行CLI-Hub 会自动检测并安装缺失的 CLI cleanup temp /tmp --dry-runcli-hub install做了什么克隆internal-tools.git仓库或使用 shallow clone 优化速度查找tools/cleanup/cli.yaml根据cli.yaml中的python:字段动态创建一个符号链接或轻量 wrapper 脚本到~/.local/bin/cleanup如果cli.yaml中声明了dependencies: [requests]则自动pip install requests隔离在用户级不影响全局环境生成补全脚本并激活。更新与依赖管理当你推送cleanup的新版本比如修复了 Windows 路径 bug团队成员只需cli-hub update cleanup # 拉取最新 cli.yaml 和源码 # 或 cli-hub update --all # 批量更新所有已安装 CLI注意CLI-Hub 不强制要求所有 CLI 都用 Python 编写。cli.yaml支持shell:字段可指向任意可执行文件commands: - name: deploy shell: ./scripts/deploy.sh arguments: - name: env type: string default: staging3.2 为什么 CLI-Hub 比 pip install 更适合 CLI 场景对比pip install my-cleanup-tool和cli-hub install cleanup维度pip installCLI-Hub安装粒度整个 Python 包可能含大量未使用的库仅安装 CLI 所需的元数据和源码按需加载版本控制pip install my-tool1.2.0需手动指定cli-hub install cleanupv1.2.0Git tag 支持或cli-hub install cleanup#main分支多版本共存pip install my-tool1.1.0会覆盖1.2.0CLI-Hub 默认为每个 CLI 创建独立沙箱cleanupv1.1.0和cleanupv1.2.0可同时存在通过cleanup1.1切换卸载干净度pip uninstall my-tool可能残留 CLI 脚本cli-hub uninstall cleanup彻底删除~/.local/bin/cleanup及补全配置发现机制pip search已废弃依赖 PyPI 网站搜索cli-hub search log直接查询所有已配置 Hub 的cli.yaml中的description字段我在一家 200 人规模的 SaaS 公司落地过 CLI-Hub效果显著运维团队的 17 个内部工具从 Kafka Topic 清理到数据库 Schema Diff全部迁入平均 CLI 安装时间从 8 分钟手动下载、解压、配置 PATH缩短到 3 秒新员工入职时只需运行cli-hub install all-dev-tools即可获得全套开发环境 CLI不再需要阅读长达 20 页的《本地环境搭建指南》。4. CLI-Anything 的底层引擎元数据驱动的动态 CLI 路由器是如何工作的理解 CLI-Anything 的核心不在于它提供了什么功能而在于它如何以极低的开销实现这些功能。它的架构可以概括为三层元数据层Declarative Layer、加载层Loading Layer、路由层Routing Layer。这三层共同构成了一个“CLI 操作系统”的雏形。4.1 元数据层cli.yaml 是一切的源头cli.yaml不是简单的配置文件而是一个经过严格 schema 校验的领域特定语言DSL。其核心字段设计直指 CLI 开发的痛点name: mytool # CLI 命令名也是安装后的可执行文件名 version: 2.1.0 # 语义化版本用于 CLI-Hub 更新策略 description: My awesome tool # 用于 --help 和 CLI-Hub 搜索 author: devcompany.com license: MIT # 全局选项所有子命令共享 global_options: - name: verbose type: integer short: v default: 0 help: Increase verbosity (can be used multiple times: -vvv) # 子命令定义 commands: - name: serve description: Start a local web server python: mytool.server:run_server arguments: - name: port type: integer short: p default: 8000 help: Port to bind to - name: host type: string default: 127.0.0.1 help: Host to bind to # 子命令专属选项 options: - name: debug type: boolean short: d help: Enable debug mode - name: build description: Build project artifacts shell: ./scripts/build.sh arguments: - name: target type: string required: true help: Build target (e.g., prod, dev)关键设计点python:和shell:字段的二元性明确区分“Python 函数调用”和“外部进程执行”避免了传统框架中subprocess.run()与import混用的混乱short:字段的显式声明CLI-Anything 会自动为每个参数生成-p、-d等短选项无需用户在代码里重复定义type:字段的语义化integer、boolean、string、path、choice等类型不仅用于运行时校验还直接驱动补全行为例如choice类型会在TAB时列出所有可选值global_options的统一注入--verbose这类通用选项只需定义一次CLI-Anything 会自动将其注入到所有子命令的解析逻辑中且保证--verbose的计数逻辑-vvv→verbose3在所有命令中一致。4.2 加载层动态导入与沙箱隔离当用户执行mytool serve --port 3000时CLI-Anything 的加载层启动解析 CLI 调用链识别出主命令mytool、子命令serve、参数--port 3000定位 cli.yaml在mytool的安装目录或 CLI-Hub 缓存目录中查找cli.yaml动态导入目标模块# 伪代码 module_path, func_name mytool.server:run_server.split(:) module importlib.import_module(module_path) # 动态导入 mytool.server target_func getattr(module, func_name) # 获取 run_server 函数参数绑定与类型转换将--port 3000字符串解析为int将--verbose计数转换为int对path类型参数自动调用Path()构造对choice类型校验输入值是否在允许列表中沙箱执行CLI-Anything 会为每次 CLI 调用创建一个最小化的执行上下文确保sys.argv、os.environ等全局状态不会被污染。这对于需要多次调用不同 CLI 的自动化脚本至关重要。注意CLI-Anything 的加载层完全不依赖setuptools的entry_points。它绕过了 Python 包安装的整套机制直接操作模块导入因此能支持.py文件、.zip包、甚至远程 URLpython: https://raw.githubusercontent.com/user/repo/main/tool.py:main这是pip install无法做到的灵活性。4.3 路由层统一的 CLI 解析引擎与补全协议CLI-Anything 的路由层是其最精妙的部分。它没有为每个 CLI 工具单独实现一套argparse而是构建了一个通用的、可插拔的解析引擎。该引擎的核心是一个CommandRouter类其工作流程如下元数据预编译在cli-any build阶段CLI-Anything 会读取cli.yaml将其编译为一个内部的CommandTree数据结构。这个树节点包含命令名、描述、参数列表、子命令列表、以及一个指向“执行器”的闭包closure。运行时路由当 CLI 被调用时CommandRouter根据sys.argv[1:]逐级匹配CommandTree直到找到叶子节点即最终要执行的函数。补全协议生成CLI-Anything 定义了一套标准的补全协议Completion Protocol。对于 bash它生成一个_mytool函数对于 zsh它生成一个_mytoolcompletion script。这些脚本不硬编码任何逻辑而是调用cli-any complete --command mytool --argv $由 CLI-Anything 的路由层实时返回补全建议。这意味着补全内容永远与cli.yaml保持一致choice类型参数的补全项是动态计算的例如git checkout TAB会实时列出所有分支用户可以编写自定义补全插件只要遵循协议即可。我在调试一个choice类型参数的补全问题时发现CLI-Anything 的路由层会将cli.yaml中的choices: [prod, staging, dev]直接序列化为 JSON然后由补全脚本解析。这比argcomplete的choices参数需要在 Python 运行时动态计算更可靠因为补全发生在 shell 层不依赖 Python 环境。5. 实战从零开始用 CLI-Anything 封装一个 Python 爱心代码并发布到 CLI-Hub网络热词里反复出现的“python爱心代码”常被初学者用来练习语法但它其实是一个绝佳的 CLI-Anything 入门案例——因为它足够简单几行代码又足够典型需要参数控制大小、颜色、输出格式。我们将它封装成一个名为love的 CLI 工具并发布到一个模拟的 CLI-Hub。5.1 步骤一编写核心逻辑完全不关心 CLI创建love.py# love.py def draw_heart(size: int 5, color: str red, output_format: str text): Draw a heart shape with specified size and color. Args: size: Size multiplier (1-10) color: Color name or hex code output_format: text or svg if not (1 size 10): raise ValueError(Size must be between 1 and 10) if output_format text: # Simple ASCII heart heart [ * (size-1) ♥ * (size-1), * (size-2) ♥ ♥ * (size-2), ♥ * (size-1) ♥ * (size-1) ♥, ♥ * (2*size-1) , ] for line in heart: print(line.replace(♥, f\033[1;31m♥\033[0m if color red else f\033[1;32m♥\033[0m)) elif output_format svg: # Generate simple SVG svg fsvg width{size*40} height{size*40} xmlnshttp://www.w3.org/2000/svg path dM{size*20},{size*10} C{size*10},{size*5} {size*5},{size*15} {size*10},{size*25} C{size*15},{size*35} {size*25},{size*35} {size*30},{size*25} C{size*35},{size*15} {size*30},{size*5} {size*20},{size*10} Z fill{color} strokeblack stroke-width1/ /svg print(svg) else: raise ValueError(fUnknown format: {output_format}) if __name__ __main__: # 这里只是用于直接运行测试CLI-Anything 不会调用此块 draw_heart()注意这个文件没有任何argparse或typer代码它就是一个纯粹的、可被直接import的 Python 模块。5.2 步骤二编写 cli.yaml 描述文件创建同目录下的cli.yamlname: love version: 1.0.0 description: Draw beautiful hearts in your terminal or as SVG author: your-nameexample.com license: MIT global_options: - name: quiet type: boolean short: q help: Suppress non-essential output commands: - name: show description: Draw a heart in the terminal python: love.py:draw_heart arguments: - name: size type: integer short: s default: 5 help: Heart size (1-10) - name: color type: string short: c default: red help: Color name (red, green, blue) or hex code (#FF0000) options: - name: format type: choice choices: [text, svg] short: f default: text help: Output format - name: version description: Show version information python: love.py:__version__ # 这里我们故意留个坑love.py 没有 __version__CLI-Anything 会优雅报错5.3 步骤三构建、测试、安装# 1. 安装 CLI-Anything pip install cli-anything # 2. 初始化并构建 cli-any init # 会检测到 love.py 和 cli.yaml生成基础配置 cli-any build # 生成 ~/.local/bin/love 可执行文件 # 3. 测试 love show --size 3 --color green --format text love show -s 7 -c #0000FF -f svg heart.svg # 4. 验证补全zsh 用户 source (love completion zsh) love show TAB # 应该列出 --size, --color, --format 等5.4 步骤四发布到 CLI-Hub模拟创建一个空 Git 仓库https://github.com/yourname/cli-love-hub将love.py和cli.yaml推送到main分支的根目录在本地配置 CLI-Hubcli-hub add love-hub https://github.com/yourname/cli-love-hub.git安装cli-hub install love # 现在 love 命令全局可用实操心得在cli.yaml中定义version字段时我最初直接写1.0结果 CLI-Hub 更新失败。后来发现 CLI-Hub 内部使用packaging.version.Version进行比较它要求版本号必须是 PEP 440 兼容格式如1.0.01.0会被视为LegacyVersion无法进行语义化比较。这是一个典型的“文档没写但实测踩坑”的细节CLI-Anything 的文档里应该强调这一点。6. 常见陷阱与避坑指南那些 CLI-Anything 文档里不会写的实战经验CLI-Anything 的设计理念是“简单”但任何工具在真实生产环境中都会遇到边界情况。以下是我在多个项目中总结的、最具杀伤力的五个陷阱以及对应的解决方案。6.1 陷阱一Windows 下的路径分隔符与 Unicode 控制字符冲突现象在 Windows 上love show --color blue输出的心形乱码部分字符显示为方块。根因CLI-Anything 在 Windows 上默认启用colorama进行 ANSI 转义但colorama.init()与某些 PowerShell 版本的 Unicode 处理存在冲突导致\033[1;34m这类转义序列被错误解析。解决方案在cli.yaml中添加windows_compatibility配置# cli.yaml windows_compatibility: enable_ansi: true force_color: false # 强制禁用彩色输出避免冲突或者在调用时显式禁用love show --color blue --no-color经验不要迷信“跨平台”标签。CLI-Anything 的跨平台能力很强但 Windows 的终端生态PowerShell vs CMD vs Windows Terminal极其碎片化。我的建议是在cli.yaml中始终为windows_compatibility字段提供默认值并在 CI 中用 GitHub Actions 的windows-latestrunner 进行回归测试。6.2 陷阱二CLI-Hub 的缓存污染导致旧版本 CLI 无法更新现象cli-hub update love显示“Already up to date”但love --version仍显示旧版本。根因CLI-Hub 使用git clone --depth 1进行浅克隆但git的 shallow clone 无法获取所有 tags。当你在远端仓库打了一个新 tagv1.1.0本地缓存的 shallow clone 里没有这个 tagcli-hub update就认为没有新版本。解决方案强制刷新完整克隆cli-hub clean love # 清理 love 的本地缓存 cli-hub install love # 重新安装这次会执行完整 clone或者配置 CLI-Hub 使用--no-shallow选项需 CLI-Hub v0.8.0cli-hub config set hub.love.no_shallow true6.3 陷阱三shell:命令中的环境变量未被正确继承现象cli.yaml中定义了一个shell: ./deploy.sh但deploy.sh里echo $PATH发现缺少~/.local/bin。根因CLI-Anything 在执行shell:命令时为了安全默认使用一个最小化的env只包含PATH、HOME、USER等基本变量不继承当前 shell 的所有自定义变量。解决方案在cli.yaml中显式声明需要继承的变量commands: - name: deploy shell: ./deploy.sh environment: - PATH - MY_CUSTOM_VAR - PYTHONPATHCLI-Anything 会自动将这些变量的值注入到子进程的env中。6.4 陷阱四choice类型参数的补全在 zsh 下不工作现象love show --format TAB在 zsh 下没有补全选项但在 bash 下正常。根因zsh 的补全系统zshcompinit需要cli-any completion zsh生成的脚本被正确 sourced且zstyle配置必须启用_cli_anythingcompleter。解决方案检查~/.zshrc是否包含# 确保 compinit 已加载 autoload -Uz compinit compinit # 确保 CLI-Anything 补全被启用 zstyle :completion:* completer _complete _ignored _approximate zstyle :completion:* matcher-list r:|[._-]* r:|* l:|* r:|* # 最关键的一行 zstyle :completion:* users $(whoami)然后重新加载source ~/.zshrc。6.5 陷阱五CLI-Anything 与 Poetry 环境的冲突现象在一个用 Poetry 管理的项目中cli-any build生成的 CLI 无法找到项目依赖。根因CLI-Anything 的动态加载默认使用系统 Python 解释器而非 Poetry 创建的虚拟环境。解决方案在cli.yaml中指定 Python 解释器路径# cli.yaml python_interpreter: .venv/bin/python # Linux/macOS # python_interpreter: .venv\\Scripts\\python.exe # Windows或者更推荐的方式是在 Poetry 项目中将 CLI-Anything 作为 dev-dependency并在pyproject.toml中配置[tool.poetry.dev-dependencies] cli-anything ^0.9.0 [tool.cli-anything] # 这样 cli-any 命令就会在 Poetry 的虚拟环境中运行这些陷阱每一个都曾让我在深夜的终端前抓耳挠腮半小时以上。它们不会出现在官方文档的“Quick Start”里但却是决定一个 CLI 工具能否在真实团队中落地的关键。CLI-Anything 的强大不在于它没有缺陷而在于它的设计足够透明让你能快速定位、理解并绕过这些缺陷。