从“能跑“到“能打“:我把Shell脚本踩过的坑,攒成了这篇避坑指南

📅 2026/7/2 1:21:15
从“能跑“到“能打“:我把Shell脚本踩过的坑,攒成了这篇避坑指南
一、 开头那几行决定了它是玩具还是工具很多人的脚本开头是这样的#!/bin/bash echo start...这在自己电脑上玩玩没问题但在生产环境这就像没系安全带开车。企业级脚本的标准开头我一般是这么写的#!/usr/bin/env bash # Author: YourName # Date: 2025-05-21 # Desc: 定时清理7天前的日志文件 set -euo pipefail IFS$\n\t这几行代码的作用每一个都是血泪教训换来的#!/usr/bin/env bash比写死#!/bin/bash更聪明。它会从环境变量$PATH里找bash无论是CentOS、Ubuntu还是Alpine都能正确运行避免了在我这能跑在服务器上报错的尴尬。set -e这是最重要的安全网。任何命令返回非0失败脚本立刻退出。防止错误被忽略一路滚雪球到最后酿成大祸。set -u使用了未定义的变量直接报错退出。还记得我开头的那个惨案吗就是因为$dir没赋值脚本执行了rm -rf /。加上这个如果$dir为空脚本会在执行删除前就挂掉。set -o pipefail管道命令的守护神。比如grep error log | head如果grep没找到匹配项返回1默认情况下整个管道返回0因为head成功了。加上这个参数管道中任意一环失败整个管道都算失败。IFS$\n\t防止文件名带空格导致脚本把my file.txt拆成my和file.txt两个参数。二、 变量与参数那些看不见的陷阱1. 永远给变量加双引号这是一个好习惯能解决90%的诡异问题。# 错误示范 file$1 rm -f /tmp/$file # 正确姿势 file$1 rm -f /tmp/$file如果不加引号当$1包含空格时系统会把它拆成多个参数。更严重的是如果$1为空错误示范就变成了rm -f /tmp/后果不堪设想。2. 参数校验不要相信用户的输入不要假设用户一定会按你的要求传参。usage() { echo Usage: $0 source_dir backup_dir exit 1 } # 检查参数个数 if [[ $# -ne 2 ]]; then usage fi src$1 dst$2 # 检查源目录是否存在 if [[ ! -d $src ]]; then echo Error: Source directory $src does not exist. exit 1 fi养成写usage函数的习惯并在脚本开头校验所有输入。这是专业和业余的分水岭。3. 命令替换的现代写法# 老派写法不推荐 todaydate %Y%m%d # 现代写法推荐 today$(date %Y%m%d) backup_filebackup_${today}.tar.gz$()结构清晰支持嵌套可读性更强。三、 流程控制别让逻辑变成迷宫1. if判断用对工具判断数值和字符串语法是不同的混用会导致逻辑错误。# 数值比较推荐用双括号 if (( num 10 )); then echo 大于10 fi # 字符串比较用双方括号 if [[ $name admin ]]; then echo Welcome admin fi # 文件判断 if [[ -f $file ]]; then echo 是普通文件 elif [[ -d $dir ]]; then echo 是目录 fi记住(( ))用于算术运算[[ ]]用于字符串和文件判断。它们比单括号[ ]更强大也更少出错。2. for循环处理文件名带空格的情况# 潜在危险 for file in *.txt; do process $file done # 安全写法 for file in *.txt; do process $file done # 更严谨的写法防止没有匹配时循环体仍被执行 shopt -s nullglob for file in *.txt; do gzip $file done shopt -u nullglobnullglob选项确保如果没有.txt文件for循环根本不会执行避免了把通配符*.txt当作一个字面量参数传给gzip。3. while读文件正确处理每一行# 错误会按空格拆分一行内容 cat file.txt | while read line; do ... done # 正确一次读取一整行包括空格 while IFS read -r line; do echo Processing: $line done file.txtIFS清空分隔符-r防止反斜杠被解释为转义字符。这是读取文件的标准答案。四、 文本三剑客从会用到精通1. grep不止是搜索# 搜索多个关键字 grep -E error|fail|exception app.log # 显示匹配行的前后各5行排查异常上下文神器 grep -B5 -A5 OutOfMemoryError app.log # 只输出匹配的部分正则表达式 grep -oP \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} access.log2. sed在线编辑的艺术# 替换文件内容Linux sed -i s/old/new/g file.txt # 替换文件内容macOS兼容写法 sed -i s/old/new/g file.txt # 替换指定行的内容 sed -i 5s/old/new/ file.txt⚠️ 警告使用sed -i前最好先备份。可以把-i改成-i.bak这样会在修改前生成一个.bak备份文件。3. awk数据统计的瑞士军刀# 统计Nginx日志中各IP访问次数取Top 10 awk {print $1} access.log | sort | uniq -c | sort -rn | head -10 # 更高效的awk原生写法 awk {count[$1]} END {for(ip in count) print count[ip], ip} access.log | sort -rn | head -10 # 计算平均响应时间假设最后一列是响应时间 awk {sum$NF; cnt} END {if(cnt0) printf Avg: %.2f ms\n, sum/cnt} access.log五、 实战进阶写一个健壮的部署脚本下面是一个更接近生产环境的脚本示例融合了前面提到的许多技巧#!/usr/bin/env bash set -euo pipefail APP_DIR/opt/myapp BACKUP_DIR/opt/backups/myapp_$(date %Y%m%d_%H%M%S) ROLLBACK0 # 清理函数无论脚本如何退出都会执行 cleanup() { if [[ $ROLLBACK -eq 1 -d $BACKUP_DIR ]]; then echo 部署失败开始回滚... rm -rf $APP_DIR mv $BACKUP_DIR $APP_DIR systemctl restart myapp || true elif [[ -d $BACKUP_DIR ]]; then echo 部署成功备份保留在: $BACKUP_DIR fi } trap cleanup EXIT echo 开始部署应用... # 1. 备份当前版本 echo 备份当前版本到 $BACKUP_DIR... cp -a $APP_DIR $BACKUP_DIR # 2. 模拟部署新版本如果这些步骤任何一步失败脚本会因set -e退出并触发cleanup回滚 echo 拉取最新代码... git -C $APP_DIR pull origin main || { ROLLBACK1; exit 1; } echo 安装依赖... npm --cwd $APP_DIR ci --production || { ROLLBACK1; exit 1; } echo 重启服务... systemctl restart myapp || { ROLLBACK1; exit 1; } echo 部署完成这个脚本的精髓在于trap cleanup EXIT和ROLLBACK变量。无论脚本是正常结束还是中途出错都会执行cleanup函数。如果出错它会自动回滚到备份版本实现了部署的事务性。六、 调试与优化专业选手的工具箱1. 增强版调试输出export PS4${BASH_SOURCE}:${LINENO}: set -x # 你的脚本内容这样在调试模式下输出的每一行前面都会显示文件名和行号方便定位问题。2. 使用shellcheck进行静态分析在把脚本放到生产环境前先用shellcheck检查一下。# 安装 # yum install ShellCheck 或 apt install shellcheck shellcheck your_script.sh它会像编译器一样指出你的脚本中可能存在的语法错误、不良实践和潜在的bug。把它集成到你的CI/CD流程中能拦截绝大部分低级错误。七、 总结Shell脚本看似简单门槛极低但写好、写稳、写得安全却是一门学问。它不需要你掌握复杂的算法但需要你对细节有近乎偏执的关注。安全第一set -euo pipefail和变量双引号是标配。防御性编程永远校验输入假设一切外部条件都可能出错。善用工具grep/sed/awk的组合拳能解决大部分文本处理问题。优雅退出trap不仅能捕获信号更能做最后的清理和回滚。从那个rm -rf的惨痛教训到现在能写出自动回滚的部署脚本我走过的弯路希望你能避开。Shell脚本是你管理Linux最锋利的瑞士军刀善待它它就会成为你最得力的助手。你们在写Shell脚本时遇到过哪些离谱的Bug或者有什么私藏的独门技巧欢迎在评论区一起交流让我们共同提升。