我最近把自己日常工作流里的大部分重复操作都搬进了终端这套习惯我管它叫“CLI-Anything”。说白了就是一句话任何需要在电脑上重复做的事先问自己一句——能不能用一行命令搞定能就绝不点鼠标。这个思路听起来有点“折腾”但实际用下来收益非常大。以前我每天要开Idea、切分支、跑脚本、归档文件、做统计、发请求每件事至少三五次点击加起来非常可观。现在这些都收敛成了十来个小命令随用随调还能写进脚本串成流水线。这篇文章就分享一下我是怎么设计这套“CLI-Anything”体系的包含完整的骨架、可直接抄的实战配方以及一路踩过的坑。适合给想提升终端效率、对脚本自动化感兴趣的人参考哪怕你只写过几行shell也能跟着搭起属于自己的一版。1. 先聊聊“一切皆可CLI”这件事1.1 为什么我宁愿敲命令也不点按钮终端看起来“不友好”但它有一个GUI永远比不上的优势它处理的是纯文本流。文本就可以被组合、被管道传递、被脚本判断、被工具补全。你今天手动操作一次的步骤写成命令后可以无限次、一模一样地重复执行。举个例子以前我要把一周的代码提交记录整理成周报需要打开Git客户端、翻提交历史、手动汇总变更文件再复制到文档里排版至少十分钟。现在一条命令跑完自动生成Markdown格式的统计表连备注都带好。区别不在于这十分钟本身而在于“这件事有没有被固化下来”。GUI操作每次都是新的而命令是一次性投资之后每次使用都是纯收益。当然我并不是说CLI要完全取代图形工具。看diff、找冲突、浏览仓库历史的时候图形界面反而更直观。我的原则很简单高频、确定、可批处理、带副作用的事情优先CLI化需要探索、需要视觉对比的事情保留GUI。1.2 适合CLI化任务的判断标准不是所有事情都值得封装成命令我总结了一个四维判断表可以参考维度适合CLI化的特征不适合CLI化的特征频率每周至少用几次偶尔才做一次的事确定性步骤固定输入输出明确每次都需要临场判断批处理涉及多个文件或多步操作单一文件、一次点击就完事副作用会创建、移动或修改文件纯查看类操作GUI更方便我自己有一个经验如果一个操作你连续手动做了三次就该考虑写命令了。如果这个操作还可能被同事复现那就更值得脚本化。成本很低一条命令通常几十行但一次封装能省下未来无数次重复劳动。我见过一些人走向另一个极端——所有事情都要脚本化结果花两小时做一个本来两分钟就能点完的活儿。判断标准其实是“性价比”手动做一次要2分钟每周做一次封装命令如果只要半小时那就值得如果手动做一次只要5秒钟那还不如继续点鼠标。2. CLI-Anything 的设计骨架2.1 命令的三层结构输入、路由、执行我最初写脚本是想到什么写什么很快发现脚本一多就乱套。后来重新搭建了一套统一结构所有命令都按照“三层”来组织第一层是输入层负责接收参数、读取配置文件、解析选项。第二层是路由层根据子命令名字分发到具体逻辑块。第三层是执行层每个子命令对应一个函数实现真正的业务逻辑。这样的好处有两个第一新加命令时不需要重新理解整套逻辑复制一个函数骨架改名字就行第二查找问题时定位非常快一看路由就能知道问题出在哪个函数里。举个我实际用的骨架例子文件名叫anything.sh放在~/bin下面#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) CONFIG_FILE${SCRIPT_DIR}/anything.conf usage() { cat EOF anything - 把重复操作浓缩成命令 用法: anything command [options] 命令: git Git 工作流快捷操作 report 生成工作统计报告 files 批量文件处理 scaffold 生成项目模板 http 发送 HTTP 请求 timer 专注计时与提醒 选项: -h, --help 显示帮助信息 -v, --verbose 输出详细日志 --dry-run 只打印将要执行的命令不真正执行 EOF } load_config() { if [[ -f ${CONFIG_FILE} ]]; then # shellcheck source/dev/null source ${CONFIG_FILE} fi } main() { load_config [[ $# -eq 0 ]] usage exit 0 local cmd$1 shift case ${cmd} in git) cmd_git $ ;; report) cmd_report $ ;; files) cmd_files $ ;; scaffold) cmd_scaffold $ ;; http) cmd_http $ ;; timer) cmd_timer $ ;; -h|--help|help) usage ;; *) echo 未知命令: ${cmd} 2 usage exit 1 ;; esac } main $这个骨架看着简单但已经把三个关键点都覆盖了统一的帮助入口、配置加载、子命令路由。你完全不需要用什么编程框架bash足够撑起这个量级。2.2 配置、参数与安全模式的设计我踩过很多坑之后总结出几个“不得不做”的设计决策。第一配置集中管理不散落在脚本各处。文件操作、Git仓库地址、目录前缀、默认超时时间这些值都放到anything.conf文件里脚本内统一引用。这样换电脑或者换项目时只需改一个文件。配置形式就是普通的KEYvalue用source加载简单直接。注意配置文件的变量名最好全部大写避免和系统环境变量冲突比如PROJECT_ROOT、WORKSPACE_DIR。第二参数解析要有优先级。我的规则是命令行参数 环境变量 配置文件 脚本内置默认值。这样既能开箱即用又能按场景覆盖还方便自动化脚本在大循环里临时改参数。第三--dry-run是必须有的。凡涉及移动、删除、写入的命令我都会在最前面加一个全局DRY_RUN判断。开启后命令只打印“将要执行什么”不真正执行。这个设计对调试和刚接手脚本的人特别重要可以无负担地试跑流程。具体实现我放在一个公共函数里run_cmd() { if [[ ${DRY_RUN:-0} 1 ]]; then printf [dry-run] %s\n $* return 0 fi eval $* }注意这里用了eval如果你要照抄一定要确保传给它的参数是你自己固定写好的命令模板不要直接把$1这种外部输入丢进去。更稳妥的写法是把一条完整命令通过数组传进来run_cmd() { local -a cmd($) if [[ ${DRY_RUN:-0} 1 ]]; then printf [dry-run] %q\n ${cmd[]} return 0 fi ${cmd[]} }这样既能看到即将执行的命令又安全。后面每个子命令都通过这个函数执行调试任何问题都只需要加一个--dry-run参数。2.3 为什么统一输出格式这么重要脚本写多了以后你会发现真正让人烦躁的不是功能而是每个脚本的输出风格都不一样有的用echo有的用printf有的完全不输出。统一输出格式是提升体验的关键一步。我定了一套简单的规则正常信息输出到标准输出用printf带固定前缀错误信息输出到标准错误用2重定向只有加-v或--verbose时才输出调试细节。前缀也固定下来比如[OK]、[SKIP]、[ERR]。这样脚本跑起来扫一眼输出就知道哪步成功、哪步被跳过、哪里出错了日志看起来非常舒服。统一输出格式最大的价值还不是好看而是让脚本的“可观察性”大幅提升。你可以把输出直接重定向到日志文件出问题的时候不再需要猜测是卡在哪一步而是直接看最后一行[ERR]对应的步骤。3. 六个拿来即用的实战配方3.1 Git 工作流一键化Git操作是我最先CLI化的一批因为太高频了。我现在每天的开分支、提PR、清理合并分支、查工作区状态全都被压缩成一个anything git子命令。完整的函数大概长这样cmd_git() { local action$1 shift case ${action} in start) local branch${1:-feature/$(date %Y%m%d)} git checkout -b ${branch} ;; pr) local base${1:-main} local current_branch current_branch$(git rev-parse --abbrev-ref HEAD) git push -u origin ${current_branch} printf 创建 PR: %s - %s\n ${base} ${current_branch} gh pr create --base ${base} --fill ;; cleanup) local merged_branches merged_branches$(git branch --merged | grep -v ^\* | grep -v -E (main|master|dev)$ || true) if [[ -z ${merged_branches} ]]; then echo 没有需要清理的分支 return 0 fi for branch in ${merged_branches}; do run_cmd git branch -d ${branch} done ;; *) usage_git ;; esac }这里有一个非常重要的安全细节git branch -d用的是小写-d不是大写的-D。小写只允许删除已经合并过的分支如果分支上还有未合并的提交Git会拒绝删除。这个保护机制能防止手滑把还没merge的工作给删了。cleanup操作我建议保留这一步保护不追求极致自动化。配合自动补全效果更好我后面会单独讲。3.2 一键生成周报与统计报告写周报是很多人的痛点其实数据都在Git里只是你没把它“挖”出来。我用一个命令按指定时间范围统计提交记录然后生成Markdown表格cmd_report() { local since_date${1:-$(date -v-7d %Y-%m-%d 2/dev/null || date -d -7 days %Y-%m-%d)} local report_filework-report-${since_date}.md { echo # 工作周报 echo echo ## 提交统计自 ${since_date} 起 echo echo | 日期 | 提交数 | 主要变更 | echo | --- | --- | --- | git log --since${since_date} --prettyformat:%ad|%s --dateshort \ | awk -F| {print | $1 |1| $2 |} \ | sed s/|1|/| /2 } ${report_file} echo [OK] 已生成周报: ${report_file} }由于各平台date语法不一样我上面写了兼容macOS和Linux的写法先试-v-7d失败就退回-d。这是跨平台shell脚本最常见的一个坑。执行后打开生成的.md文件基本就是可直接提交的周报底稿。再配合一个全局weekly_report的alias每周五下班前跑一次一分钟搞定。3.3 批量文件重命名与归档批量处理文件是CLI最能发光发热的领域。我之前最常做的一件事是把下载目录里的散乱文件按日期归档。刚开始我也直接用for f in $(ls)后来发现文件一多、名字一复杂就翻车。正确做法用find配合while read处理注意把文件路径作为单独数组元素传入绝不把路径拼进字符串再靠空格切分cmd_files() { local target_dir$1 [[ -z ${target_dir} ]] echo 用法: anything files 目录 2 exit 1 local today today$(date %Y-%m-%d) while IFS read -r -d file; do local basename ext prefix basename$(basename ${file}) ext${basename##*.} prefix${basename%%.*} case ${ext} in jpg|jpeg|png|gif|webp) prefiximg ;; pdf|docx|doc|xlsx) prefixdoc ;; zip|tar|gz|7z) prefixarchive ;; mp4|mov|mkv) prefixvideo ;; *) prefixmisc ;; esac local new_name new_name${today}_${prefix}_${basename} run_cmd mv -n ${file} ${target_dir}/${new_name} done (find ${target_dir} -maxdepth 1 -type f -print0) echo [OK] 文件归档完成目标目录: ${target_dir} }补充几个细节-print0和read -d 搭配专门处理带空格、带换行的文件名这是最稳妥的遍历方式。mv -n即--no-clobber在目标文件已存在时不会覆盖避免重复运行脚本时把新文件覆盖掉。文件名加了日期前缀作为“操作标识”同一批文件就算重复归档到同一个目录也会保留原始文件名不会互相覆盖。我把这个脚本设计成幂等的同一批文件跑两次第二次会因为目标已存在而跳过不会产生二次重命名。归档完之后还可以串一个统计命令出来find ${target_dir} -maxdepth 1 -type f | wc -l这就是CLI的第二个优势——管道组合。每个命令只做一件事但组合起来就能完成一个完整流程。3.4 项目脚手架生成器新建项目时总得手写一堆目录结构和配置文件我干脆写了个anything scaffold子命令用heredoc生成模板文件。假设我要快速初始化一个带基础目录结构和README的Python项目cmd_scaffold() { local project_name$1 [[ -z ${project_name} ]] echo 用法: anything scaffold 项目名 2 exit 1 if [[ -d ${project_name} ]]; then echo [ERR] 目录已存在: ${project_name} 2 exit 1 fi run_cmd mkdir -p ${project_name}/{src,tests,docs,scripts} run_cmd touch ${project_name}/.gitignore cat ${project_name}/README.md EOF # ${project_name} 项目初始化时间$(date %Y-%m-%d %H:%M:%S) ## 结构说明 - src/: 核心代码 - tests/: 测试 - docs/: 文档 - scripts/: 常用脚本 ## 快速开始 ... EOF echo [OK] 项目脚手架已生成: ${project_name} }也许你觉得这不就是几行mkdir吗确实是但它固定了目录结构的规范防止每次新项目都凭感觉建目录。模板文件用heredoc生成天然支持变量替换比写多个echo高效得多。我建议把这类模板命令放进一个单独目录比如~/.templates每个项目类型存一份模板文件夹命令做成anything scaffold 项目名 模板名会灵活很多。3.5 开发时的番茄钟与提醒命令开发时最容易忘记的是“该起来活动一下了”或者“那个任务该check一下了”。我用命令实现了一个极简的番茄钟思路是用date和sleep做计时到点后调用系统通知。macOS和Linux的通知命令不一样我做了平台判断cmd_timer() { local minutes${1:-25} local total$((minutes * 60)) echo [OK] 定时 ${minutes} 分钟开始计时... sleep ${total} if [[ $(uname) Darwin ]]; then osascript -e display notification 休息时间到 with title 番茄钟 else notify-send 番茄钟 休息时间到 fi echo [OK] 时间到 }这个命令虽然简单但配合一个终端音频播放效果会更好。你可以把sleep和afplaymacOS或paplayLinux串起来到点出声提醒。命令行最强大的地方就是这些看似普通的命令可以无缝组合。3.6 把 HTTP 请求封装成命令行工具我经常要调试一些接口一条条拼curl太累而且参数换一下就得重敲一遍。封装成命令之后把URL结构、认证头、超时时间都固定下来只暴露变化的部分。cmd_http() { local method${1:-GET} local endpoint$2 shift 2 local headers(-H Authorization: Bearer ${TOKEN}) local params() while [[ $# -gt 0 ]]; do case $1 in -d|--data) params(-d $2) shift 2 ;; -j|--json) params(-H Content-Type: application/json -d $2) shift 2 ;; -t|--timeout) params(--max-time $2) shift 2 ;; *) echo [ERR] 未知参数: $1 2 exit 1 ;; esac done run_cmd curl -sf ${method} ${endpoint} ${headers[]} ${params[]} \ | jq . || { echo [ERR] 请求失败或返回非JSON 2 } }顺带提两个细节curl -s静默模式但保留错误信息curl -f在HTTP错误码时返回非零状态配合jq美化JSON输出。这个命令可以进一步扩展成“一键打开接口文档”“一键对比两次响应diff”都能很轻松实现。4. 踩过的坑和调试技巧4.1 文件名空格导致脚本原地爆炸这是shell脚本第一坑。早期我写过for f in $(ls *.pdf)这种代码只要某个文件名带空格就会被拆成两个词脚本立刻行为异常。正确做法是所有文件名、路径都不经过空格切分通过数组传递并且用-print0配合read -d 遍历。所有变量都加双引号比如mv $file $target绝不写成mv $file $target。这条规则我遵守了之后脚本出错的概率直接下降一半。4.2 管道让 set -e 完全失效set -e是让脚本在遇到错误时立即退出的保险但它在管道里经常“失效”。比如set -e some_command | grep pattern即使some_command失败了grep退出码是0管道整体退出码就是0脚本会继续跑。这会酿成很隐蔽的错误——看起来成功实际上前面那步挂了。解决办法是在脚本开头加set -o pipefail让管道只要有一个环节失败就算失败。我现在所有脚本统一写set -euo pipefail这个组合里面还包含set -u能捕获未定义变量的引用很多笔误就是它救回来的。这个组合对新手来说可能有点严格任何一处没处理好都会让脚本退出。但恰恰是这个“严格”逼着你把脚本写健壮而不是靠运气跑。4.3 从其他目录调用命令导致路径全乱如果不做任何处理脚本里写的相对路径都相对当前工作目录。你从/home/user/project目录运行没问题但换个目录就全部失效。我的标准解法就是在脚本开头取一次脚本自身所在目录之后所有路径都基于它SCRIPT_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd) CONFIG_FILE${SCRIPT_DIR}/anything.conf然后需要访问的目录统一写成${SCRIPT_DIR}/xxx。这一个习惯能解决大量“在我机器上明明可以”的问题。4.4 命令幂等性的重要性最初我写的归档脚本第二次运行会出问题原因是第一次运行已经加了前缀第二次又加一层。后来我明确了所有有副作用的命令都尽量设计成“可重复执行”。几个常用技巧创建目录用mkdir -p重复执行不会报错。移动文件用mv -n目标已存在就跳过。删除文件前先判断存在性比如[[ -f $file ]] rm $file。在执行操作前先把目标状态打印出来尤其用--dry-run调试验证。幂等还有一个额外好处你可以放心把它放进定时任务不用每次小心翼翼确认上一次执行结果。4.5 调试工具组合bash -x shellcheck真的出问题时我基本靠三个工具排查第一个是bash -x anything.sh它会打印每条命令的实际执行轨迹变量会展开为真实值。这样能立刻看到程序走的哪条分支、传了什么参数。第二个是shellcheck。这是一个静态分析工具会直接告诉你哪行有隐患、为什么。它给出的解释往往比你自己排查更快尤其是新手写shell时。第三个是在脚本里安插DEBUG级别的日志。我在统一输出方案里加了一个verbose开关平时不显示加-v才打印每条关键操作的细节。排查问题时把这个开关打开相当于给脚本装了一个操作记录仪。log_debug() { [[ ${VERBOSE:-0} 1 ]] printf [DEBUG] %s\n $* }这三个工具加起来能解决99%的shell脚本问题。4.6 别忽略自动化补全这个“最后一公里”CLI体验里最容易被忽略的是自动补全。没有补全你每次都要想命令名和参数名有了补全肌肉记忆慢慢形成效率会明显提升。我用bash的complete实现最简单的补全。先定义子命令列表然后绑定到补全函数_anything_complete() { local cur prev cur${COMP_WORDS[COMP_CWORD]} prev${COMP_WORDS[COMP_CWORD-1]} local commandsgit report files scaffold http timer help if [[ ${COMP_CWORD} -eq 1 ]]; then COMPREPLY( $(compgen -W ${commands} -- ${cur}) ) return 0 fi case ${prev} in git) COMPREPLY( $(compgen -W start pr cleanup -- ${cur}) ) ;; files) COMPREPLY( $(compgen -d -- ${cur}) ) ;; esac } complete -F _anything_complete anything写一行source ~/.bashrc或者放到~/.bash_completion目录里新开终端就能用。这样每个子命令的合法候选参数都能自动提示不用记。最后补充一点实操中的经验CLI-Anything听起来像是一个“把所有事情命令行化”的宏大工程但实际上门槛远没有想象中高。我自己就是从第一个最烦人的功能开始改造的当时每周归档一次下载目录手动了两个月终于受不了写了一个40行的脚本从此再也没用鼠标做过这件事。然后自然蔓延到Git、翻报告、发请求越用越顺手。给想尝试的朋友一个建议不要追求一上来就建一整套框架先挑一件你最烦的重复操作用最简单的方式写成一个命令跑起来再说。等积累到四五个命令时你会发现它们之间天然有共性那时候再统一公共函数都不迟。我的经验是真正帮你省时间的不是某个命令多高效而是这套“把重复工作固化下来”的思维习惯。动手写第一个吧哪怕只是把两个cd拼成一个命令也行。