ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CLI-Anything:用Shell脚本打造终端里的全能命令工具箱

CLI-Anything:用Shell脚本打造终端里的全能命令工具箱 1. 整体设计与思路拆解为什么非要“Anything”进命令行先交代背景。我给自己定的目标是凡是每天要重复做两次以上的事全部搬进终端凡是能用一段脚本说清楚的操作绝不用鼠标点三遍。于是就有了“CLI-Anything”这个个人项目——严格来说它不算一个正经开源软件而是一套约定、一组脚本、一个能让我“在命令行里干任何事”的工作流框架。核心关键词就是CLI-AnythingCLI 是 Command Line InterfaceAnything 意味着不设边界把文件处理、批量重命名、日志分析、服务启停、甚至日常琐事全部纳入统一命令层。这个项目解决的核心痛点其实是“上下文割裂”。我试过很多效率工具番茄钟、文件整理软件、自动化脚本但它们各自为政有的藏在 GUI 里有的要单独记一套快捷键真正忙起来根本想不起来用。而终端是唯一一个你每天一定会打开、且天然支持组合和编排的环境。与其在多个工具之间来回切换不如把所有高频操作收敛成一套 CLI 命令mybat rename、mybat backup、mybat todo add统一入口、统一帮助、统一日志用起来就像在跟一台懂你的机器对话。这套方案适合谁首先是日常和命令行打交道的开发者、运维、数据分析师他们已经有终端使用习惯缺的只是一套把零散命令整合成“个人工具箱”的方法其次是刚接触命令行的新人——通过模仿这个项目能快速理解 Shell 脚本、参数解析、退出码这些概念到底怎么串在一起。不适合谁如果你日常工作全是表格、文档、PPT且没有意愿碰终端那确实没必要强制 CLI 化工具是为人服务的别本末倒置。我见过很多人对命令行有误解觉得它是“老古董”或者“高手专属”。实际上 CLI 最大的优势不是炫技而是三个字可编程。你在图形界面里的每一次点击都是一次性的而命令行里的每一条命令都可以被保存、复用、拆分、组合。我把这个理念叫做“一切皆命令”它直接决定了我后面所有工具选择、脚本结构和命名规范。2. 地基搭建Shell 环境与基础工具链选型2.1 Shell 的选择Zsh 还是 Bash区别在哪先别急着写脚本第一步要确定用哪个 Shell。默认情况下macOS 用 Zsh绝大多数 Linux 发行版用 BashWindows 上则推荐通过 WSL 或 Git Bash 来获得类 Unix 环境。我个人选择 Zsh 作为日常交互 Shell但所有脚本统一用 Bash 写。为什么这么折腾原因有两个其一交互体验。Zsh 的补全、提示符、目录跳转比如cd a/b/c之后输个...就能回退确实比原生 Bash 舒服尤其是长时间泡在终端里这些细节能明显降低疲劳感。其二可移植性。脚本最终可能要跑在不同机器上甚至交给同事跑一遍。Bash 是事实标准几乎每个 Linux 发行版都自带。而 Zsh 的某些语法特性比如高阶的数组操作、特定补全函数换到 Bash 环境就会报错。因此我的约定是交互用 Zsh脚本用 Bash两者互不混用避免脚本在本机能跑、换台机器就挂的尴尬。还要注意 Shell 版本。Bash 3.2macOS 默认和 Bash 5.xLinux 默认在数组、关联数组上有差异。如果你用了declare -A定义关联数组在 macOS 上就必须bash /usr/local/bin/bash或改用其他方式实现。这是个很隐蔽的坑我第一次写批量脚本时就在这上面卡了半小时。2.2 必备工具盘点不全求多但求顺手CLI-Anything 不是把网上的工具装一堆就行。工具链的选择标准只有一个能覆盖 80% 日常场景且互相之间能通过管道和文件协作。我这里列一下自己的基础清单find、grep、sed、awk文本与文件处理的基本盘。尤其是grep -r配合-E正则能解决绝大多数“找东西”的需求jqJSON 解析神器。没有它处理 API 返回数据就得写一坨 Python有了它一行搞定fzf模糊查找器。配合 Ctrl-R 搜索历史命令、配合vim $(fzf)选择文件体验是质变级别的ripgreprg比grep更快而且默认尊重.gitignore在大型代码仓库里搜索体验远远好过传统 greptmux终端复用。 session、窗口、窗格三个层级跑长任务不怕断连shellcheck静态检查 Shell 脚本。新手写脚本最容易踩各种引号、空格坑它能直接告诉你哪里不对。这些工具都不是我为项目专门装的但它们组合起来就构成了“Anything”的底层素材库。命令本身是死的组合方式才是活的。举个例子你要查代码仓库里所有 TODO 注释并按文件统计数量rg -n TODO|FIXME --type-add src:*.{js,ts,py,go} -tsrc -g !test/** | \ cut -d: -f1 | sort | uniq -c | sort -rn这行命令用到了 rg、cut、sort、uniq 四个工具各自只做一件事连起来就是一条高效的统计流水线。CLI-Anything 工具箱的核心训练其实就是训练这种“管道思维”小工具各司其职通过标准输入输出协作。2.3 参数设计的学问为什么统一命名和帮助信息这么重要走到这一步你就可以考虑搭自己的命令骨架了。我强烈建议从一开始就统一风格每个子命令都要支持-h/--help参数采用--keyvalue或--key value的 GNU 风格退出码遵循 0 成功、非 0 失败的约定。如果每个脚本有不同的参数风格、不同的帮助格式记忆负担会爆炸工具箱很快就变成一堆谁也不愿意翻的杂物堆。实操上我会先写一个公共函数库lib.sh专门负责解析参数、打印帮助、记录日志。比如解析参数可以用一个很朴素的手法#!/usr/bin/env bash # lib.sh 公共函数库 parse_args() { while [[ $# -gt 0 ]]; do case $1 in -h|--help) show_help exit 0 ;; -v|--verbose) VERBOSE1 shift ;; --path*) TARGET_PATH${1#*} shift ;; --path) TARGET_PATH$2 shift 2 ;; *) echo 未知参数: $1 2 exit 2 ;; esac done } log() { local level$1 shift if [[ $level DEBUG $VERBOSE -ne 1 ]]; then return fi echo [$(date %Y-%m-%d %H:%M:%S)] [$level] $* } # 脚本退出时统一清理 cleanup_on_exit() { # 可以在这里删除临时文件、恢复目录 : } trap cleanup_on_exit EXIT这个函数库的好处是所有命令的“骨架”一致写新命令时不用重复做参数解析帮助信息统一格式日志级别统一控制。以后每加一个新命令只需要写具体的业务逻辑剩下的胶水代码全从 lib.sh 里继承。这本质上就是一个“脚手架思维”用最小成本做出一个可扩展的结构。3. 核心实现打造可复用的命令层3.1 统一入口设计利用 bin 目录与软链接理论说得再多不落地都是废话。CLI-Anything 的物理形态其实很朴素一个~/bin目录内部放一堆独立脚本然后在~/.bashrc或~/.zshrc里加一行export PATH$HOME/bin:$PATH。所有命令都通过~/bin下的软链接或别名暴露给系统比如我建了一个主命令叫any后面跟子命令~/bin/any ├── lib.sh # 公共库 ├── any # 主入口脚本 ├── any-files # 文件处理子命令 ├── any-system # 系统运维子命令 ├── any-todo # 待办清单子命令 └── any-cron # 定时任务管理子命令用软链接而不是把所有代码堆在一个大文件里是为了让每个子命令可以独立开发、独立测试。写坏了某个命令不会影响其他。同时每个子命令都遵循同样的解析逻辑入口文件any本身几乎不做事它只是找到并调用对应子命令脚本#!/usr/bin/env bash # any 主入口按子命令分发 set -euo pipefail COMMAND_NAME${1:-} if [[ -z $COMMAND_NAME ]]; then echo 错误缺少子命令。用法: any files|system|todo|cron [options] 2 exit 1 fi shift CMD_SCRIPT$HOME/bin/any-$COMMAND_NAME if [[ ! -f $CMD_SCRIPT ]]; then echo 错误未知子命令 $COMMAND_NAME 2 exit 1 fi exec bash $CMD_SCRIPT $set -euo pipefail这一行要单独说一下-e让脚本遇到错误就退出-u让变量未定义即报错-o pipefail让管道命令中只要有一个环节失败整条管道的退出码就是失败。这三件套能规避一大票 Shell 脚本的隐蔽问题新手写脚本第一行就应该是它。至于用exec是为了让子命令进程取代当前进程避免多出一层嵌套CtrlC 的信号传递也更干净。这个“主入口 子脚本”的结构我实测下来最舒服的一点是新加一个命令的流程被压缩到了 1 分钟以内。复制一个模版脚本改改业务逻辑加个软链接完事。没有繁琐的框架配置没有依赖安装连最基础的 Linux 机器都能跑起来。3.2 核心功能一文件批量处理实战文件批量处理是 CLI-Anything 里最常用的一块我拿“批量重命名”来拆解。需求很典型某个目录下有一堆照片命名是IMG_20240901_120000.jpg这种我想按拍摄时间重命名成20240901_120000.jpg的顺序号形式。第一反应是rename命令部分 Linux 发行版自带rename是 Perl 版本但它在不同平台语法不一致所以我更倾向于自己写一个既通用又可控的脚本。核心逻辑分三步先定位目标文件再解析和生成新名字最后确认并执行。第三步里有两个细节值得注意一是要先把所有重命名对收集到数组里、做冲突检查后再逐个执行不然边改边数容易出错二是要支持 dry-run 预览模式先在终端打印结果但不真正改动文件。#!/usr/bin/env bash # any-files rename批量重命名 source $HOME/bin/lib.sh DRY_RUN0 PATTERN TARGET_DIR parse_args $ mapfile -t files (find $TARGET_DIR -maxdepth 1 -type f) if [[ ${#files[]} -eq 0 ]]; then log WARN 目录为空没有文件需要处理 exit 0 fi # 先收集重命名对做冲突检查 declare -A seen for f in ${files[]}; do base$(basename $f) # 这里用一个简单的示例把 IMG_YYYYMMDD_HHMMSS 转成 YYYYMMDD_HHMMSS new_base$(echo $base | sed -E s/^IMG_([0-9]{8})_([0-9]{6})\.(.*)$/\1_\2.\3/) if [[ -z $new_base || $new_base $base ]]; then log DEBUG 跳过 $base无需处理或无匹配模式 continue fi new_path$TARGET_DIR/$new_base if [[ -n ${seen[$new_base]:-} ]]; then log ERROR 检测到重名冲突: $new_base 将同时被 $seen[$new_base] 和 $f 生成 exit 1 fi seen[$new_base]$f rename_pairs($f|$new_path) done # 预览或执行 for pair in ${rename_pairs[]}; do old_path${pair%%|*} new_path${pair##*|} if [[ $DRY_RUN 1 ]]; then echo [dry-run] $old_path - $new_path else mv $old_path $new_path fi done if [[ $DRY_RUN 1 ]]; then log INFO 以上是预览结果去掉 --dry-run 后才会真正执行 else log INFO 共重命名 ${#rename_pairs[]} 个文件 fi这里用了mapfile而不是$(find ...)目的就是避免“文件名里有空格”时被拆成多个数组元素。很多人写 Shell 脚本踩坑十有七八是栽在空格和通配符上用mapfile -t按行读取能根治这个问题。declare -A seen拿来做冲突检测也非常关键——日常使用时重名是最高频的意外情况。3.3 核心功能二日常事务自动化示例除了文件处理“Anything”更吸引人的一面是把日常琐事收拢成命令。我自己每天都用的一个功能是待办管理any todo add 写周报、any todo list、any todo done 3。所有数据存成一个纯文本 Markdown 文件不引入任何数据库。为什么不用现成的 Todo 工具因为我想让待办数据像代码一样可以被 grep、可以进版本库、可以自己写脚本统计历史趋势。实现上很简单数据文件就是~/notes/todo.md每行一条格式为- [ ] 任务描述。脚本的核心操作就是正则匹配和文本追加#!/usr/bin/env bash # any-todo极简待办管理 source $HOME/bin/lib.sh TODO_FILE${TODO_FILE:-$HOME/notes/todo.md} ensure_file() { mkdir -p $(dirname $TODO_FILE) [[ -f $TODO_FILE ]] || touch $TODO_FILE } list_todos() { grep -n \- \[[ x]\] $TODO_FILE | sed s/^- \[.\] // } add_todo() { echo - [ ] $* $TODO_FILE log INFO 已添加: $* } done_todo() { local line_no$1 sed -i ${line_no}s/- \[ \]/- [x]/ $TODO_FILE log INFO 已完成第 ${line_no} 条待办 }sed -i 的是 macOS 的坑BSD sed 要求-i后必须跟一个后缀参数留空代表不备份。Linux 的 GNU sed 则是sed -i直接搞定。这个差异几乎人人都会遇到所以我在脚本里加了判断或统一用#!/usr/bin/env bash配合变量处理。如果你主要的跑脚本环境是 Linux这句可以直接用sed -i。实际上这个 todo 系统虽然简陋却有一个 GUI 工具不具备的优势它能放进 Git 仓库。每次修改后自动提交你就有了一整条时间线哪个任务哪天完成的一周后回顾一目了然。同理它也可以被其他命令消费——比如生成 markdown 周报、统计待办完成率全部复用同一份数据源。4. 进阶扩展把服务、脚本、工作流统一进 CLI4.1 把业务脚本封装成命令层很多人积累了一堆 Python 脚本、SQL 查询、运维命令但每次都靠“翻历史命令”来找等于没有沉淀。CLI-Anything 的第二层进阶是把这些零散脚本“收敛”成规范子命令。比如我有一个 Python 脚本专门抓取内网服务状态原始运行方式是python3 /opt/scripts/check_internal.py --env prod。封装之后变成any system check --env prod。封装技巧主要有两条。一是不要把原脚本拖进~/bin而是保留原目录在命令脚本里通过绝对路径调用它。这样原脚本的日志文件、虚拟环境路径、依赖关系不会被破坏CLI 层只是门口的接待员。二是注意环境变量传递。如果原脚本依赖某个 TOKEN 或 API Key要优先读~/.config/any/env这类统一配置文件而不是把密钥硬编码进命令脚本。我自己的习惯是建一个~/.config/any/环境变量文件chmod 600 权限然后所有命令在启动时 source 它# any-system 内部片段 source $HOME/.config/any/secret_env.sh # 里面 export 各种密钥 python3 /opt/scripts/check_internal.py --env $ENV_NAME这样做的直接收益是密钥只在 4~5 个地方出现统一管理别人看你的命令脚本时看不到敏感信息要换环境变量时只改一个文件。4.2 组合拳管道、子命令和参数穿透CLI-Anything 最迷人的地方在于组合。单看any-files rename --dry-run --path~/Photos只是一个工具但它输出一段旧路径 - 新路径的文本这段文本可以被任何其他命令继续消费。比如我想把所有重命名后的文件名同步到一个清单里可以这样any files rename --dry-run --path~/Photos | grep - | awk {print $3} /tmp/new_names.txt再比如配合fzf选择一个待办来编辑any todo list | fzf --preview echo 选中条目可编辑 | while read -r task; do echo 你选中了: $task done参数穿透也值得一提。我在子命令里会保留一个双横线之后的原样参数区把无法枚举的参数直接透传给底层工具。比如any curl -- -w %{http_code} https://example.com里的--后面的内容全部拼接到 curl 命令尾部。这个设计要想清楚不是所有参数都需要提前解析保持百分之二十的“透传能力”能让命令层的生命力远超预期。4.3 定时任务让命令在后台“自运转”把命令接入 cron 或 systemd timer是 CLI-Anything 从“手动工具箱”走向“自动化帮手”的一步。比如我每天早上 9 点自动跑一遍内部服务的健康检查并把结果写入~/notes/health.log。核心不是一个花哨的 cron 配置而是你要保证命令本身具备“可无人值守性”命令结束时必须能给出正确的退出码这样 cron 才能通过邮件或通知捕捉异常命令日志要带时间戳且追加写入不能每次都覆盖上次结果命令要容忍外部依赖暂时不可用重试三次之后再报错。我在 common 库里写了一个retry函数自动化任务都会包一层retry() { local n1 local max3 local delay5 while true; do $ break if [[ $n -ge $max ]]; then log ERROR 重试 $max 次后仍然失败: $* return 1 fi log WARN 第 $n 次执行失败${delay} 秒后重试 sleep $delay n$((n 1)) done }于是 crontab 里只需要一行0 9 * * * /home/user/bin/any system check --env prod /home/user/notes/health.log 21。日志文件的21别漏cron 环境下标准错误很多工具都不默认输出漏掉会把异常悄无声息吞掉。5. 常见问题与排查技巧实录5.1 高频故障速查表下面这张表是我自己在维护这套工具箱时踩过频率最高的几个坑。现象常见原因解决思路找不到any命令~/bin没进 PATH或没重新 source 配置文件检查~/.bashrc里的 export执行source ~/.bashrc文件名带空格导致参数错乱使用了未加引号的$或数组遍历统一使用$、${files[]}引号包裹macOS 上报sed: illegal option -- iBSD sed 与 GNU sed 不兼容改用sed -i 或统一用 Perl 一行命令脚本运行到一半报未定义变量set -u下引用了未设置的环境变量给所有变量赋默认值如${VAR:-default}管道中的一个命令失败但整体返回 0没有pipefail在脚本开头加set -euo pipefail系统重启后命令消失环境变量写错了文件或没持久化确认写入的是.bashrc/.zprofile而非终端临时会话第一行的“找不到命令”我遇到过十次以上而且每次都是不同的环境细节装了新终端模拟器、换了一台机器、或者某个 SSH 会话没有加载交互式 shell 配置。排查办法很简单执行echo $PATH看有没有~/bin没有就补有就检查软链接创建权限。绝大多数此类“灵异事件”都是 PATH 或软连接问题不是脚本本身写错了。5.2 调试技巧shellcheck 与 x-trace 双保险调试 Shell 脚本最土也最有效的办法是bash -x your_script它会逐行打印命令展开后的实际执行情况。配合一个精心设计的日志级别DEBUG 输出变量值基本上任何逻辑错误都能定位。比如变量名拼写错误、正则没匹配上、路径引用错误在-x输出里立刻现形。我在lib.sh里保留了DEBUG档位的日志日常不显示出问题时--verbose一把梭。另一个强烈建议是给新写的脚本跑一遍shellcheck。它是一个静态检查工具能揪出 90% 以上的低级问题比如[ $foo 1 ]忘了给变量加引号、用test命令替代[[ ]]导致空变量报错、在直接用source外部文件前没有检查文件是否存在。对这个工具的态度我用一句话概括在你认为自己很懂 Shell 之前它比你更懂在你认为自己很懂之后它能帮你省掉查文档的半小时。5.3 个人心得宁可慢一点也要统一返回码与文档最后分享一个绕不开的工程经验为每个命令写好-h帮助并给重要命令维护 README。CLI-Anything 最大的优势是“一切都能在终端里完成”但这要求你对自己工具箱的状态了如指掌。曾经我写了一个any deploy命令当时只有一个参数三周后加了四个参数同时又改了配置文件路径结果自己都记不清了。从那以后我定了一条规矩新命令可以晚点上线帮助信息不能晚写。帮助内容不用长两三行说明用途加参数列表和示例就够。然后定期跑一遍any --help看看全部命令列表哪个命令的说明含糊顺手改掉。另一个心得是关于退出码的。这个看不见摸不着的东西实际决定了自动化任务能不能稳定运行。最初我写脚本时经常exit 0一劳永逸导致某次备份脚本失败了cron 却认为是成功的没发任何告警。后来我统一改成成功返回 0参数错误返回 2运行时错误返回 1重试耗尽返回 3而外部调用方比如 cron完全靠这些数字判断下一步动作。不要小看这个“数字”它就是你跟其他程序约定互动的语言。整套 CLI-Anything 做到这个程度本质上已经是“个人数字化生产线”的雏形了。我也建议你尝试一下先列出你每天重复滚瓜烂熟的三五个操作用~/bin目录把它们变成命令再逐步扩展。工具不在多能稳稳当当地解决你身边的事情这套东西就算成了。我在实际使用中体会最深的一点是每次想到一个重复操作就立刻花五分钟把它变成命令日积月累之后你的工具箱会以你意想不到的速度长出自己的生态来。顺手分享一个实用技巧在主入口any中加一个统计子命令统计每个子命令被调用的次数这样你就能清楚知道哪些命令最值得优化和扩展。CLI-Anything 不是终点它是一台可以一直打磨下去的个人效率引擎。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进