☰
CLI-Anything:用Shell脚本打造终端里的全能命令工具箱
2026/9/28 22:16:54 网站建设 项目流程

1. 整体设计与思路拆解:为什么非要“Anything”进命令行

先交代背景。我给自己定的目标是:凡是每天要重复做两次以上的事,全部搬进终端;凡是能用一段脚本说清楚的操作,绝不用鼠标点三遍。于是就有了“CLI-Anything”这个个人项目——严格来说它不算一个正经开源软件,而是一套约定、一组脚本、一个能让我“在命令行里干任何事”的工作流框架。核心关键词就是CLI-Anything:CLI 是 Command Line Interface,Anything 意味着不设边界,把文件处理、批量重命名、日志分析、服务启停、甚至日常琐事全部纳入统一命令层。

这个项目解决的核心痛点其实是“上下文割裂”。我试过很多效率工具:番茄钟、文件整理软件、自动化脚本,但它们各自为政,有的藏在 GUI 里,有的要单独记一套快捷键,真正忙起来根本想不起来用。而终端是唯一一个你每天一定会打开、且天然支持组合和编排的环境。与其在多个工具之间来回切换,不如把所有高频操作收敛成一套 CLI 命令:mybat rename、mybat backup、mybat todo add,统一入口、统一帮助、统一日志,用起来就像在跟一台懂你的机器对话。

这套方案适合谁?首先是日常和命令行打交道的开发者、运维、数据分析师,他们已经有终端使用习惯,缺的只是一套把零散命令整合成“个人工具箱”的方法;其次是刚接触命令行的新人——通过模仿这个项目,能快速理解 Shell 脚本、参数解析、退出码这些概念到底怎么串在一起。不适合谁?如果你日常工作全是表格、文档、PPT,且没有意愿碰终端,那确实没必要强制 CLI 化,工具是为人服务的,别本末倒置。

我见过很多人对命令行有误解,觉得它是“老古董”或者“高手专属”。实际上 CLI 最大的优势不是炫技,而是三个字:可编程。你在图形界面里的每一次点击都是一次性的,而命令行里的每一条命令都可以被保存、复用、拆分、组合。我把这个理念叫做“一切皆命令”,它直接决定了我后面所有工具选择、脚本结构和命名规范。

2. 地基搭建:Shell 环境与基础工具链选型

2.1 Shell 的选择:Zsh 还是 Bash,区别在哪

先别急着写脚本,第一步要确定用哪个 Shell。默认情况下,macOS 用 Zsh,绝大多数 Linux 发行版用 Bash,Windows 上则推荐通过 WSL 或 Git Bash 来获得类 Unix 环境。我个人选择 Zsh 作为日常交互 Shell,但所有脚本统一用 Bash 写。为什么这么折腾?原因有两个:

其一,交互体验。Zsh 的补全、提示符、目录跳转(比如cd a/b/c之后输个...就能回退)确实比原生 Bash 舒服,尤其是长时间泡在终端里,这些细节能明显降低疲劳感。

其二,可移植性。脚本最终可能要跑在不同机器上,甚至交给同事跑一遍。Bash 是事实标准,几乎每个 Linux 发行版都自带。而 Zsh 的某些语法特性(比如高阶的数组操作、特定补全函数)换到 Bash 环境就会报错。因此我的约定是:交互用 Zsh,脚本用 Bash,两者互不混用,避免脚本在本机能跑、换台机器就挂的尴尬。

还要注意 Shell 版本。Bash 3.2(macOS 默认)和 Bash 5.x(Linux 默认)在数组、关联数组上有差异。如果你用了declare -A定义关联数组,在 macOS 上就必须bash /usr/local/bin/bash或改用其他方式实现。这是个很隐蔽的坑,我第一次写批量脚本时就在这上面卡了半小时。

2.2 必备工具盘点:不全求多,但求顺手

CLI-Anything 不是把网上的工具装一堆就行。工具链的选择标准只有一个:能覆盖 80% 日常场景,且互相之间能通过管道和文件协作。我这里列一下自己的基础清单:

  • find、grep、sed、awk:文本与文件处理的基本盘。尤其是grep -r配合-E正则,能解决绝大多数“找东西”的需求;
  • jq:JSON 解析神器。没有它,处理 API 返回数据就得写一坨 Python,有了它一行搞定;
  • fzf:模糊查找器。配合 Ctrl-R 搜索历史命令、配合vim $(fzf)选择文件,体验是质变级别的;
  • ripgrep(rg):比grep更快,而且默认尊重.gitignore,在大型代码仓库里搜索体验远远好过传统 grep;
  • tmux:终端复用。 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,参数采用--key=value或--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) VERBOSE=1 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,是为了让子命令进程取代当前进程,避免多出一层嵌套,Ctrl+C 的信号传递也更干净。

这个“主入口 + 子脚本”的结构,我实测下来最舒服的一点是:新加一个命令的流程被压缩到了 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_RUN=0 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. 进阶扩展:把服务、脚本、工作流统一进 CLI

4.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 n=1 local max=3 local delay=5 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 2>&1。日志文件的2>&1别漏,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 不是终点,它是一台可以一直打磨下去的个人效率引擎。

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

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

立即咨询