☰
CLI-Anything:把零散脚本统一为一条命令的任务执行框架
2026/9/28 16:35:17 网站建设 项目流程

1. CLI-Anything想解决什么问题:从一堆零散脚本到统一入口

前阵子我清理工作目录,数了数这半年攒下的脚本:一个抓取网页标题的 Python 脚本、一个批量压缩图片的 shell 脚本、一个调第三方接口查快递的 Node 脚本、两个做数据清洗的 jupyter 转 py 文件,还有一些写了就忘的 awk 一行流。它们功能各不相同,语言横跨 Python、Bash、Node,调用方式也千奇百怪——有的要python xxx.py -u url,有的要./img_compress.sh --input ...,有的必须先进到特定目录才能运行。更要命的是,隔了两周再看,我经常想不起来某个脚本当初是干嘛的、传入参数到底要-s还是--size。

这就是 CLI-Anything 想解决的核心问题:把零散、异构、难以记忆的脚本和任务,统一收敛成一组命名清晰、参数标准、可以在任何目录下调用的命令。

CLI-Anything 本质是一个任务定义与执行框架。它不是要替代现有脚本,而是给这些脚本包一层"壳",让你用一致的交互方式去触发它们。你可以把任意可执行的东西——shell 命令、Python 函数、HTTP API 调用、甚至一组有顺序的复合操作——声明成一个"任务",然后通过统一的命令行入口去调用。

适合读这篇文章的,是那些和我一样,工作里积攒了大量小脚本、脚本越来越多到快失控的人。不管你是后端、运维、数据分析还是做自动化的,只要你的日常里有"打开终端,敲一串不容易记住的命令"这件事,CLI-Anything 的思路就值得参考。

我把它的核心使用方式总结成一句话:写配置文件,然后敲一个单词。

clia run publish-note --title "我的新博客" --tag tech

这一条命令背后,可能是先渲染模板、再调用 Git 提交、最后触发远程构建。你不需要记得每一个步骤,CLI-Anything 会按定义好的顺序帮你执行。它最让我舒服的一点是:所有任务定义都放在同一个目录里,用一位固定的语法描述,新写的任务也只需要在这个目录里加一个文件。脚本本身可以乱,入口必须统一。

2. 核心机制拆解:任务注册、参数解析与动态分发的设计

CLI-Anything 的架构不复杂,但几个关键设计直接影响好不好用。我把它的内部拆成四层:任务注册表、参数解析器、执行分发器、输出格式化器。

2.1 任务定义:用文本描述一个命令

整个框架的基础是任务定义。CLI-Anything 没有走"写代码注册"的路子,而是采用配置文件声明的方式来描述任务。这有个好处:添加一个任务不需要修改框架本身的代码,也不需要重新部署,放一个文件进去就生效了。我用 YAML 格式来写任务定义,结构大概是这样:

name: publish-note description: 渲染博客笔记并推送到远程仓库 type: pipeline # 复合任务,按顺序执行多个步骤 steps: - type: shell cmd: "python render.py {{note_path}}" - type: shell cmd: "git add . && git commit -m 'note: {{title}}'" - type: http method: POST url: "https://api.example.com/build" headers: Authorization: "Bearer {{token}}"

name是在命令行里实际输入的指令;description会出现在帮助列表里,方便你几个月后回来查"这个命令是干嘛的";steps定义了实际要干的事。CLI-Anything 的任务类型我一般只用三种:shell执行命令、http请求接口、python执行函数。这三种覆盖了我日常 95% 的需求。

任务配置文件放在固定目录(比如~/.clia/tasks/),CLI-Anything 启动时扫描这个目录,把所有任务的名称和元信息加载到内存里的注册表。查找某个命令的时候,直接查表,不需要每次解析所有配置文件的内容,响应速度很快。

2.2 参数解析:从命令行到任务上下文的映射

参数设计是 CLI-Anything 和"普通脚本合集"拉开差距的地方。它支持一种变量模板语法:你在任务定义里用{{变量名}}声明占位,然后在命令行通过--变量名 值传入,框架会把两者对应起来。

实现上我参照了 Jinja2 的渲染方式,但简化了很多。CLI-Anything 启动时会做两件事:第一,扫描当前任务定义里出现的所有{{ }}占位符,动态生成这个任务的参数清单;第二,用 argparse 基于这份清单解析命令行参数。所以在终端里敲:

clia run publish-note --note_path ./draft.md --title "hello" --tag tech

框架内部会生成一个上下文 dict:

{ "note_path": "./draft.md", "title": "hello", "tag": "tech" }

然后所有步骤定义里的{{note_path}}都会被替换成实际值,再交给执行器。这里有个细节值得说:如果任务定义里有{{token}}这种不想每次手输的变量,怎么办?我是建议放在一个单独的secrets.yaml里,执行时自动合并进上下文。CLI-Anything 的做法是,渲染时优先取命令行传参,其次是 secrets 文件,最后是默认值。这样既不把密钥写死在任务配置里,又不用每次重复输入。

参数解析还有一个坑:布尔开关。比如--force这种没有值的参数。CLI-Anything 的做法是约定--force true、--force false,虽然不如--force直接,但换来了解析逻辑的统一——所有参数都是key value结构,不会因为某参数有没有值而走上不同分支,实现和维护成本都低很多。

2.3 执行分发器:按类型把任务交给合适的执行器

注册表拿到了参数,接下来就是执行。CLI-Anything 的执行分发器根据每个 step 的type字段,把任务分发给对应的执行器。

shell 执行器是最好写的,本质就是subprocess.run(),但有几个细节需要处理好:超时控制、工作目录、环境变量注入。工作目录默认是任务配置文件所在目录,但我在实践里发现这个默认值经常导致"脚本明明在本机跑得好好的,换了机器就找不到文件"的问题。后来我在 CLI-Anything 里给每个 step 增加了可选的cwd字段,如果不写才默认用配置文件目录。这样既保持了"配置文件目录是基准"的一致性,又给了特殊情况一个出口。

http 执行器做的事情天然简单:拼接 URL、填充请求头、发请求、拿响应码。但它真正有价值的设计是断言机制——你可以声明期望的响应码,如果实际不符,任务直接判定失败:

- type: http method: GET url: "https://api.example.com/health" expect_status: 200

这个机制让我可以把"检查服务是否正常"也封装成一个命令。之前是肉眼盯着 curl 的输出看,现在clia run check-api就能拿到明确的成功/失败结果,CI 里也可以直接引用。

python 执行器是扩展性最强的。它读取一个指定的.py文件中的某个函数,把参数 dict 传进去,拿到返回值。这个设计让我那些已有的 Python 逻辑不需要重写,只需要暴露一个函数,就能被 CLI-Anything 纳管。

2.4 输出格式化:人看和机器读是两回事

CLI-Anything 对输出的处理,是我从一开始就强调的:每个任务执行结束后,除了人类可读的日志,还必须在最后输出一行机器可读的 JSON 结果。比如:

{"status":"success","duration_ms":1234,"output":"...","data":{}}

这一行 JSON 平时用肉眼看不碍事,但当你把 CLI-Anything 接进 CI 脚本或者其他自动化流程时,它就成了标准的对接协议。我一直认为,一个命令行工具的最终价值,一半在于它能不能被其他程序可靠地调用。CLI-Anything 从一开始就定义好了成功(exit 0)和失败(exit 非 0)的协议,这让我在 shell 脚本里接它非常省心。

3. 实战:把"发布一篇带模板的博客"封装成 CLI 命令

纸上谈兵没意思,我拿一个自己实际在用的例子,完整走一遍从拆解任务到定义到调用的流程。

3.1 场景拆解:发一篇博客需要几步

我发技术博客的流程是:写 Markdown 草稿,然后要做三件事——把草稿里的图片压缩一下,把 Markdown 渲染成带站点模板的 HTML,然后推送到远程仓库触发线上构建。

这三个步骤分散在三个脚本里:compress_images.py、render_site.py,最后一步是两条 Git 命令。以前发一篇博客要敲七八条命令,而且很容易忘记先压缩图片再渲染的顺序。用 CLI-Anything 之后,我把整个过程定义成一个publish-post任务,执行顺序、参数、工作目录全部写在配置文件里。

3.2 编写完整的任务定义文件

我实际使用的配置文件长这样:

name: publish-post description: 压缩图片、渲染Markdown、推送到远程触发构建 type: pipeline params: - name: draft required: true description: Markdown 草稿文件路径 - name: title required: true - name: tag default: tech steps: - type: shell name: "压缩文章图片" cmd: "python scripts/compress_images.py --input {{draft}} --quality 80" cwd: "~/workspace/blog-toolkit" - type: shell name: "渲染HTML" cmd: "python scripts/render_site.py --draft {{draft}} --title {{title}} --tag {{tag}}" cwd: "~/workspace/blog-toolkit" - type: shell name: "提交并推送" cmd: "git add . && git commit -m 'post: {{title}}' && git push" cwd: "~/workspace/my-blog" - type: http name: "触发远程构建" method: POST url: "https://api.example.com/hooks/blog-build" expect_status: 204

注意cwd字段——不同步骤工作在不同目录,这在实际场景里非常重要,否则压缩脚本和 Git 仓库在同一个目录下根本没法工作。这是我在最初版本里没考虑到的,踩过坑之后补上的。

3.3 调用与验证

配置写完,执行就变成一句话:

clia run publish-post --draft ./draft/xxx.md --title "CLI-Anything实战" --tag dev

CLI-Anything 会按顺序执行四个步骤,每个步骤开始前打印步骤名,结束时打印用了多少毫秒。如果某一步失败,流水线立刻停止返回非 0 退出码,失败的步骤名和日志快照会被单独标出来。

我第一次跑通的时候,最大的感受不是"方便",而是**"终于不用记顺序了"**。步骤的先后顺序是写在配置里的,由框架保证执行,不存在哪次手滑忘了压缩图片就推送的情况。而且新增一个"发布前检查敏感词"的步骤,只需要在 steps 数组里插入一段配置,对原有流程毫无侵入。

3.4 处理失败与重试

真实使用中一定会遇到失败。我遇到的情况主要有三种:

第一种是远程接口偶发超时。发布博客的触发请求偶尔慢,可能超过了默认超时时间。CLI-Anything 支持给 http 步骤单独配置超时和重试次数:

- type: http method: POST url: "https://api.example.com/hooks/blog-build" timeout_sec: 30 retry: 2 expect_status: 204

第二种是** Git 推送时因为网络问题中断**。这种如果只靠框架自动重试,有时候会因为本地已经产生了 commit 而重复 commit。我的做法是不给 shell 步骤全局自动重试,只在配置里加一句retryable: true,让框架在失败时提示用户确认是否重试,而不是默默再来一次。

第三种是参数填错。比如--tag想传dev,结果手滑传成dev带了个空格。CLI-Anything 的做法是对每个参数定义类型和校验规则:type: enum、pattern: "^[a-z-]+$",校验不过直接拒绝执行。这比等到渲染阶段才发现错了要省事得多。

3.5 我在这个过程中学到的任务拆分经验

用过一段时间后,我对"什么样的步骤适合写进一个 pipeline"有了体会。一个任务的粒度,应该等于"一个逻辑上完整的操作"。发博客是一个完整操作,所以压缩图片、渲染、推送、触发构建都放进一个任务;但"压缩图片"本身如果也经常单独用,我会把它拆成独立任务,而不是只作为某个 pipeline 的内部步骤。

这样拆的好处是命令的复用性特别好。我既可以用clia run compress-images --input ...单独压缩一组图片,也可以用clia run publish-post走完整个发布流程。拆与合都靠配置文件完成,不需要改任何代码。

4. 兼容性、健壮性与性能:上线之后必须处理的三个问题

功能跑通只是第一步。CLI-Anything 要真正成为日常依赖的工具,还有三个问题绕不开。

4.1 跨平台路径与命令兼容

我的主力是 macOS,但偶尔会在 Linux 服务器上跑同样的任务。最开始我在任务定义里写了类似python scripts/xxx.py这种命令,实际执行时发现有的机器上python指向 Python 2,有的机器上只有python3。这个问题不解决,同一个配置文件换台机器就跑不了。

CLI-Anything 的解法是增加一个环境探测层:在加载任务配置之前,先检测当前平台可用的解释器、命令路径,生成一组内置变量,比如{{python}}自动解析成python3或python,{{path_sep}}自动解析成/或\。任务定义里不写死python,而是写{{python}}:

cmd: "{{python}} scripts/compress_images.py --input {{draft}}"

用户层面的体验是:同一份配置,在 macOS 和 Linux 上都能跑。Windows 我没有做完整适配,因为我的核心应用场景不涉及,但设计上把path_sep这类变量抽象出来之后,理论上只需要在每个平台上补一次探测逻辑。

这也给我一个教训:写 CLI 工具,从一开始就不要在配置里硬编码环境相关的东西。哪怕你当前只在一台机器上用,也要预料到配置可能被人分享、复制到别的环境。

4.2 错误码约定与日志可追踪

CLI-Anything 初期阶段,我的错误处理是"抛异常 + 打堆栈"。但真实使用中,用户(包括几天后的我自己)根本不关心堆栈,只想知道:哪个步骤失败了?失败原因是什么?怎么解决?

后来我把错误分为三个层级:

层级含义退出码
0成功0
1参数错误(校验不过、缺参数)2
2步骤执行失败(脚本返回非0、接口状态码不对)3
3框架内部错误(配置文件解析失败等)4

每一类错误都输出固定的前缀,比如参数错误输出[CONFIG],步骤失败输出[STEP_FAILED],框架内部错误输出[INTERNAL]。我配合grep就能在日志文件里快速定位问题。对于 shell 步骤,CLI-Anything 会抓取子进程的 stdout 和 stderr 尾部各 20 行,一并放到日志里,方便排查。

这一步做完之后,CLI-Anything 从一个"能跑的工具"变成了"跑挂了能快速定位的工具"。我建议任何人做类似的框架,都不要忽略错误码的设计——它是稳定使用的骨架。

4.3 启动时间与并发执行优化

CLI-Anything 早期是纯 Python 写的。Python 启动本来就慢,加上任务注册时要扫描目录、解析所有 YAML 文件、初始化日志,我测过一版居然要 1.2 秒才出帮助信息。对一个高频命令工具来说,这个延迟非常影响手感。之后我做了一次性能优化,思路有三条:

第一,按需解析。不再启动时解析全部配置文件,而是只读取每个文件名作为任务名,生成索引,用户敲run <name>时才解析该任务对应的文件。任务多的时候,启动时间从几百毫秒降到几十毫秒。

第二,预编译。把较简的 YAML 转成 Python 内部格式后做一次 pickle 缓存,文件没改动就直接加载缓存,省掉重复 YAML 解析的开销。

第三,并行执行独立步骤。在 pipeline 里,有些步骤之间没有依赖关系。比如"压缩图片"和"检查敏感词"完全可以并行。我在定义文件里引入了一个parallel: true的标记,框架会把这些步骤丢到线程池里同时跑,全部完成后再进入下一步。实测两个耗时步骤并行,整体耗时减少了大约 40%。

但这里要提醒一句:并行执行的前提是步骤之间确实互不影响。我一开始想当然地把所有"看起来互不影响"的步骤都并行化,结果遇到两个步骤同时写同一个临时文件,数据错乱。后来我加了一条约定:步骤要声明inputs和outputs,框架检测到输出冲突时会拒绝并行。这个设计是回归测试帮我发现的,只在真正安全的情况下并行,否则按顺序执行。

4.4 配置变更后的平滑升级和回滚

还有一个我一开始完全没考虑的问题:任务配置是用户自己写的,CLI-Anything 升级后配置格式可能变——比如我后来给某个字段改了名,旧配置就会解析失败。我的做法是内置一个简单的迁移器:每个版本如果改了配置格式,提供一个migrate子命令,自动把旧格式转成新格式,并在转换前备份原文件。

clia migrate --dry-run # 只分析不修改 clia migrate # 实际执行迁移

--dry-run这个选项我特别推荐。迁移这种操作,给人看一眼再动手,能避免很多"我不知道它改了什么"的不安。CLI-Anything 在升级后第一次运行时也会主动提示"有可用的配置迁移",而不是让用户自己发现问题。

5. 我对CLI-Anything的使用习惯和边界建议

工具做出来之后,最后聊点使用层面的东西。CLI-Anything 适合处理哪些事、不适合处理哪些事,我用了一段时间后边界感比较清楚。

适合的事情:一切"有明确步骤、参数有限、需要反复执行"的操作。部署、发布、数据备份、批量处理文件、调接口做检查,这类任务定义成命令之后省心程度是质的飞跃。

不太适合的事情:需要复杂交互的流程、需要实时人工确认的步骤、以及状态特别多的长流程。命令行本身的交互能力有限,如果任务在执行过程中可能要多次问"是否继续",塞进 CLI-Anything 里反而不自然。我现在的原则是:CLI-Anything 负责"确定性流程",交互式决策留在终端外面处理。比如任务执行到某一处需要人去看一眼结果,我就在那个步骤里停下来,输出明确的提示,而不是自动往下走。

另外有一个团队协作时的建议:如果 CLI-Anything 的任务配置是团队共用的,最好把它放进 Git 仓库管理。任务定义文件的 diff 记录,本身就是一份操作手册的变更历史。新同事加入时,让他git clone配置文件目录,跑一遍clia list,就能看到所有可用命令和描述,上手成本比之前看一堆文档低很多。

还有一个我从实际使用中养成的习惯:给每个任务写一句"反面说明"。在任务定义的description里,我会补一句"这个命令不会做 X,也不会主动做 Y"。比如发布任务我会写"不会自动备份数据库"。这样做不是因为啰嗦,而是因为我发现,工具越顺手,人越容易信任它,反而会忽略它没做的事情。把边界写清楚,能避免很多"我以为它会做"的误会。

CLI-Anything 对我最大的改变,不是少敲了几条命令,而是让我的操作流变得可以描述、可以分享、可以版本管理。以前我的"发布博客"只存在于我的记忆里,现在它是一段配置、一个命令、一份能给别人看的文档。这种感觉,大概就是工具化最好的回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询