
如果你跟我一样日常要在终端里敲一堆维护脚本那你大概率经历过这种场景项目目录里有scripts/、tools/、ops/至少三四个放脚本的文件夹里面躺着各种.sh和.py每个脚本的参数规则又都不一样。有的用--date 2025-01-01有的是-d 2025-01-01还有的直接让你传两个位置参数。想跑起来得先读半个小时的源码跑完了想找历史命令又发现当时根本没留记录。CLI-Anything 就是冲着这个痛点来的。它不是某个单一功能的小工具而是一套“把任意脚本变成标准命令行命令”的轻量框架。你只需要写一个简单的 YAML 描述文件告诉它命令名、参数、脚本路径剩下的事情——参数解析、帮助文本、错误提示、退出码——都交给 CLI-Anything。我把它定位成“终端的收纳箱”不管底层是 bash、Python、Node 还是 Go 编译出来的二进制统一挂到同一个命令树下用同一种方式调用再也不用靠脑子记住几十个入口。这篇文章会从它的设计思路讲起然后带你从零搭一个能用的命令组最后聊几个我在实际使用中踩过的坑。适合那些脚本散落、想系统化整理自动化流程的开发者和运维同学。1. CLI-Anything 是什么给零散脚本一个统一的命令行入口1.1 一个真实到令人窒息的痛点上个月我接手一个数据中台项目仓库里光是“取数”相关的脚本就有 8 个。有的脚本叫get_order_data.sh参数是--from --to --shop_id有的叫daily_order_sync.py参数是date env而且env还只能传prod、dev、test三个值。为了把这些脚本合并成一个统一入口我第一反应是写一个总调用的 shell但写着写着就发现要处理各种参数组合、帮助说明、校验逻辑比脚本本身还长。这其实是所有非工程化脚本最容易出现的问题功能是有的但使用门槛全在人的记忆力上。你今天知道sync.py要传什么三个月后你大概率忘了。更麻烦的是新人接手时看一遍脚本才知道怎么跑效率极低。1.2 核心理念把“怎么跑”交给约定CLI-Anything 的核心做法非常简单你不需要在业务脚本里写任何参数解析代码而是在外层用一份配置文件声明“这个命令叫什么、需要什么参数、参数是什么类型、执行哪一个脚本”。框架拿到配置后自动生成一个标准命令行工具的行为。也就是说它把胶水代码集中管理。业务脚本只负责接参数并执行CLI-Anything 负责解析用户输入、校验、格式化、帮助提示。这种拆分让两边都清爽脚本侧不用被一堆argparse逻辑污染调用侧不用记忆不同的参数风格。比如下面这个配置# commands.yaml name: data commands: - name: fetch description: 下载远程数据并落盘 args: - name: date required: true help: 业务日期格式 YYYY-MM-DD flags: - name: --bucket default: default help: 存储桶名称 script: scripts/fetch.py之后你可以像这样执行anything run data fetch --date 2025-06-01 --bucket prod-data如果想不起来有什么参数直接输anything run data fetch --helpCLI-Anything 就会从 YAML 里把描述和必填项列出来。这就是“约定优于配置”在终端场景里的落地版。1.3 谁适合用 CLI-Anything我用了几个星期之后觉得这几类人收益最大后端与运维日常有一堆定时任务、数据修复、部署脚本散落在服务器上用 CLI-Anything 统一挂载后很容易定位。数据分析师经常手动跑 SQL 导出、采样、校验的脚本可以用它把参数固定成语义化命令行降低误操作概率。工具链维护者团队里有内部 CLI 工具想让其他成员快速上手又不想为每个小工具单独写参数解析和帮助文档。喜欢折腾终端效率的人把常用操作比如备份数据库、启动本地环境、生成报告都收进一个命令空间里。它适合“命令数量在十几个到几十个”的场景。如果只有两三个脚本直接原样跑就行但如果超过十个参数千奇百怪用 CLI-Anything 整理一次后面每天都能省出一点时间。2. 核心设计拆解为什么这样设计2.1 声明式配置少写 70% 的命令行样板代码我最早尝试给这些脚本统一入口时在 Python 里用 argparse 写了半天然后又被要求支持参数别名、环境变量覆盖、帮助信息自动缩进。实话讲这些功能单独实现都不难但每个脚本都要重复写一遍就很消耗精力。CLI-Anything 选择用声明式配置核心原因是大部分命令入口的逻辑是重复的你告诉它参数名、类型、是否必填它就知道该怎么解析。框架内部能把这种重复逻辑收敛成统一实现自动生成的帮助甚至比手写的还整齐。举个例子一个参数有四种属性args: - name: date required: true type: string help: 业务日期 YYYY-MM-DD你在脚本里拿到的就是模板字符串里的{{date}}或者一个已解析的参数列表。不同类型它会给默认值--verbose这种布尔值会自动转换成开关。这些逻辑不会因为新加命令而再变所以脚本代码可以保持很薄。2.2 命令树按模块和子命令组织脚本如果一个工具只有五个命令那就是五条平级的东西。但真实场景往往是数据模块下有fetch、clean、backfill运维模块下有restart、logs、healthcheck。如果所有命令都平铺在一个层级命令列表会越来越长可读性急剧下降。CLI-Anything 支持“命令树”结构。最外层叫name: data下面挂commands每个命令还能再挂subcommands。最终形成的执行路径就是anything run data fetch。这个设计等效于给命令分文件夹只不过是在命名空间层面分。这种组织方式最大的好处是可记忆性。看到anything run data fetch你就知道data是领域模块fetch是对应的动作看到anything run ops logs也不会和data混在一起。2.3 统一 IO 与错误处理让脚本看起来像正经工程以前我自己写的脚本有个老大难问题有的用print打日志有的用logging有的压根没输出。混合操作时日志格式五花八门很难从一堆字符里区分哪条是错误信息、哪条是业务结果。CLI-Anything 设计了一套统一输出规范。默认情况下它会把脚本的 stdout 原样打印stderr 标红并带上前缀如果需要机器可读你可以让脚本输出 JSON框架会原样传递给调用方或写到文件。更重要的是错误处理脚本退出码非 0 时CLI-Anything 会统一显示[error] script exited with code 1并且把退出码透传出去。这让外部 cron 或 CI 集成变得很可靠。2.4 插件机制新命令可以不断加上去CLI-Anything 的框架本体不需要频繁改动。新增一个命令 在 YAML 里加一段 放一个脚本文件。这就等于一个插件机制想加新的自动化流程只需要遵循同一种声明方式其他一切由框架兜底。这样做的好处是团队协作时不会互相踩脚。你加你的sync命令我加我的report命令大家改的是同一个 YAML 的独立区块合并冲突也很少。对个人项目来说维护成本也被压到了最低。3. 从零上手构建你的第一个 CLI-Anything3.1 安装与项目初始化CLI-Anything 目前是 Node.js 实现但你可以用它执行任意语言的脚本。安装方式没什么特别的npm install -g cli-anything或者你从源码拉下来后本地npm link也行。装完后先看一下版本anything --version然后进入你的项目目录初始化一份配置骨架anything init它会在当前目录生成cli-anything.yaml和一个scripts/文件夹。你可以直接用这个文件开始定义命令。3.2 定义第一个命令我用一个最常见的场景来演示写一个greet命令调用 Python 脚本输出问候语。先建目录并创建脚本mkdir -p scripts cat scripts/greet.py EOF import sys name sys.argv[1] print(fHello, {name}!) EOF然后在cli-anything.yaml里注册name: demo commands: - name: greet description: 向指定用户打招呼 args: - name: name required: true help: 用户名 script: scripts/greet.py执行anything run demo greet --name Alice终端输出Hello, Alice!。如果你不传nameCLI-Anything 会直接拦截并提示缺少必填参数你的 Python 脚本根本不会被执行。这在多个命令并存时非常有用因为你是把校验统一收口而不是在脚本里一层一层判断。3.3 处理参数类型和布尔开关CLI-Anything 支持常见的参数类型转换。默认情况下参数都是字符串因为终端输入本来就是字符串。但你可以声明type: int、type: bool、type: list框架会自动在传给脚本前完成转换和使用。看一个带布尔开关的例子- name: build description: 构建前端 flags: - name: --minify type: bool help: 压缩构建产物 - name: --target type: list default: [web] help: 构建目标可多个 script: scripts/build.sh执行anything run demo build --minify --target web --target mobile脚本收到的参数里minify会被转成truetarget会被转成[web, mobile]。不用在脚本里处理字符串分割这又是省掉样板代码的一个点。3.4 接入外部脚本和 Shell 命令CLI-Anything 并不要求你的脚本必须是 Python 或 Node。如果脚本只是几行 shell你也可以直接在配置里把script写成一个 shell 命令字符串- name: backup description: 备份数据库 flags: - name: --db required: true help: 数据库名 - name: --output default: ./backup help: 备份存放目录 script: mkdir -p \$OUTPUT\ pg_dump \$DB\ \$OUTPUT/$DB.sql\这里有个关键点CLI-Anything 会把参数以环境变量的形式暴露给你的命令字符串。例如$OUTPUT、$DB这样在 shell 命令里拼接起来非常自然也避免了把命令字符串直接拼进去带来的注入风险。如果用独立的脚本文件框架会把参数作为命令行参数追加进去。比如上面的greet.py内部用sys.argv[1]拿到name。3.5 调试技巧看到框架到底在跑什么我在调试配置时经常用两个参数。第一个是--dry-run它可以让 CLI-Anything 把最终要执行的完整命令打出来但不真正执行anything run demo backup --db mydb --dry-run第二个是设置环境变量CLI_ANYTHING_DEBUG1让它把参数解析过程也输出成 JSON方便看类型转换和默认值到底对不对。CLI_ANYTHING_DEBUG1 anything run demo greet --name Alice这两个思路很朴素但省了我很多时间。尤其是脚本功能本身没问题、只是参数传错的时候一眼就能看出是框架的问题还是脚本的问题。4. 常见问题与排查实录4.1 命令树加载失败配置写错和路径不对CLI-Anything 启动时会读取当前目录下的cli-anything.yaml。如果在子目录里执行或者把配置文件放了别的名字会出现“没有找到命令定义”的报错。解决办法很简单先执行anything config --path看它到底在找哪个文件。我一开始踩过这个坑觉得自己明明写了配置项目为什么一直找不到。后来发现是我在项目根目录以外的路径敲了anything。建议每个项目固定一个根目录所有命令都在这里执行。如果某个脚本必须在另一个目录运行可以在配置里加一个cwd字段让框架先切到目标目录再启动脚本。4.2 脚本执行权限导致“Permission denied”在 Linux 和 macOS 下如果你的script直接指向.sh文件它必须带上执行权限chmod x scripts/build.sh如果忘了这个你会看到permission denied。CLI-Anything 没有替你做这一步因为它不想悄悄修改你的文件权限。这种错误看起来像工具坏了其实只是系统权限问题。另一个办法是把script写成bash scripts/build.sh这样即使没执行权限也能跑。4.3 参数值里有空格陷阱在引号终端命令天然把空格当成参数分隔符。如果你要传给脚本的值本身带空格比如--message Hello WorldCLI-Anything 会正确地作为单个值传进去。但如果你在 shell 命令字符串里直接引用$MESSAGE请务必带双引号echo $MESSAGE不要写echo $MESSAGE。前者把 “Hello World” 当一个字符串输出后者会被拆成两个词然后被 echo 打出来中间多个空格。这个问题不是 CLI-Anything 特有的是 shell 的老规矩。4.4 一个脚本同时被多个命令调用注意环境变量覆盖CLI-Anything 执行命令时会为每个参数生成一个环境变量变量名默认和参数名一致。如果两个命令用同一个参数名但含义不同比如一个--name是用户名、另一个--name是表名脚本里如果直接读$NAME会有歧义。我的建议是给参数起一个带作用域的名字比如--user_name和--table_name或者在脚本文件内部统一接收位置参数不依赖环境变量。这属于“命令设计”层面的问题配置越清晰后面排查越轻松。4.5 排查速查表现象最常见原因快速检查方式找不到命令定义配置文件路径不对anything config --path脚本返回 Permission denied文件没有执行权限ls -l scripts/*传参丢一半值里有空格但没加引号用--dry-run或CLI_ANYTHING_DEBUG1查看最终命令帮助信息没显示命令名写错或未缩进检查 YAML 缩进用anything list奇怪的退出码脚本里没有 propagate exit在脚本最后加上exit $?4.6 调试 YAML 的缩进和引号我的经验是绝大多数配置没生效都是 YAML 缩进写错了。CLI-Anything 解析 YAML 很严格commands下面的列表项缩进不对就会让整个命令树加载失败。我养成了一个习惯写完 YAML 先用一个工具校验比如python -c import yaml,sys; yaml.safe_load(open(cli-anything.yaml)); print(ok)如果没有 Python 环境也可以直接执行anything list它会在加载失败时把 YAML 解析错误信息打出来。这类报错往往指明了行号对照修改很快。5. 我用的几个小技巧和扩展方向5.1 用 group 命令减少重复输入CLI-Anything 允许在配置里加一个env区块定义固定的环境变量。如果你的所有命令都要访问同一个服务地址就把地址放在这里env: API_HOST: https://api.example.com LOG_LEVEL: info所有由 CLI-Anything 启动的脚本都会自动继承这些变量。这样你就不用在每个命令的脚本里再读一次环境变量。5.2 命令别名短到不用思考我给常用命令加了别名。比如anything run demo build --minify太长我可以在配置里设置aliases: dmb: demo build --minify之后直接输anything dmb。这只是个人口味但确实让效率再往上提了一截。5.3 后续扩展让命令自动生成脚本模板CLI-Anything 自带了一个scaffold插件可以基于你的 YAML 定义生成空的脚本文件anything scaffold demo greet --lang python这会在scripts/里生成一个已经带好参数接收逻辑的 Python 文件比如自动argparse解析或者读取sys.argv。我推荐先跑一次 scaffold然后只填业务逻辑。这样能保证脚本侧和 YAML 侧的约定一致不用回头改。5.4 把它接进 CI/CD我现在会在 CI 流水线里用 CLI-Anything 跑数据校验和部署命令。因为整个命令入口是确定的流水线配置文件里只写anything run ops deploy --env production这比直接写一串ssh和scp更安全也更易读。后续要改部署逻辑只改服务端的命令配置流水线文件几乎不动。6. 踩过几次坑后的体会我实际用下来最直接的感受是整理命令配置文件本身花不了多少时间但它带来的收益是长期的。最明显的是我现在不需要“想起”命令的样子只要知道模块名和动作名配合--help就能把脚本用起来。这对那些一个月才跑一次的冷门脚本尤其关键因为人脑的临时记忆根本留不到下一次。如果你也想把自己那堆“只有自己能看懂”的脚本收进统一入口我建议你从最小的一两个命令开始不要一上来就全部迁移。先挑一个你每周必跑的脚本写进 CLI-Anything跑顺手了再慢慢扩大。工具本身不复杂复杂的是坚持维护入口的秩序。把每天的重复操作用一条清晰、可记忆的命令封装起来时间长了你会感谢这个决定。