如果你跟我一样,日常要在终端里敲一堆维护脚本,那你大概率经历过这种场景:项目目录里有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 --help,CLI-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-Anything
3.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(f"Hello, {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!。如果你不传name,CLI-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会被转成true,target会被转成["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_DEBUG=1,让它把参数解析过程也输出成 JSON,方便看类型转换和默认值到底对不对。
CLI_ANYTHING_DEBUG=1 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 World",CLI-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_DEBUG=1查看最终命令 |
| 帮助信息没显示 | 命令名写错或未缩进 | 检查 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,跑顺手了再慢慢扩大。工具本身不复杂,复杂的是坚持维护入口的秩序。把每天的重复操作用一条清晰、可记忆的命令封装起来,时间长了你会感谢这个决定。