Shell脚本自动化:10个实战技巧
你有多少次在终端里重复执行同样的命令?
备份数据库、清理日志、部署代码、监控磁盘……这些活儿干多了,人就会累,而且容易出错。
写个脚本吧。但很多人写的脚本只能跑一次就废了——没有错误处理、没有日志、参数写死、换个环境就报错。
下面这10个技巧,是我这些年踩坑总结出来的。每个都来自真实生产场景,不是教科书废话。
技巧1:开头加上这些"保命符"
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
这三行是脚本的保险带。
set -e:任何命令报错,脚本立刻停止。别等它把错误链条拉得越来越长。
set -u:未定义的变量直接报错。很多脚本的诡异bug就是这么来的。
set -o pipefail:管道里只要有一个命令失败,整个管道就算失败。不然你以为 false | true 会返回成功,其实真的会。
IFS=$'\n\t':限制分隔符为换行和Tab,避免文件名里的空格搞事情。
有人觉得这些太严格了。但在生产环境里,宽松等于埋雷。
技巧2:函数化一切
别把所有逻辑堆在一个文件里。拆成函数,每个函数只做一件事。
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}
check_disk() {
local usage
usage=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if (( usage > 90 )); then
log_error "磁盘使用率过高:${usage}%"
return 1
fi
log_info "磁盘使用率正常:${usage}%"
return 0
}
函数的好处不只是好看。它让你能单独测试每个功能,出问题时能快速定位。
技巧3:用 getopts 解析参数
别自己手动解析 $1、$2。用 getopts,标准、简洁、不容易出错。
while getopts "d:h:v" opt; do
case $opt in
d) directory="$OPTARG" ;;
h) help_message; exit 0 ;;
v) verbose=true ;;
\?) echo "无效选项: -$OPTARG" >&2; exit 1 ;;
esac
done
用的时候:
./backup.sh -d /data -v
清晰,可读,别人一看就知道怎么用。
技巧4:日志分级,别全往 stdout 打
LOG_LEVEL="${LOG_LEVEL:-INFO}"
log() {
local level="$1"
shift
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $*" >> "$LOG_FILE"
if [[ "$LOG_LEVEL" == "DEBUG" ]]; then
echo "[$timestamp] [$level] $*"
fi
}
生产环境默认 INFO,调试时设 LOG_LEVEL=DEBUG。日志写到文件里,重要信息才输出到屏幕。
技巧5:临时文件要管理好
TMPDIR=$(mktemp -d /tmp/my_script.XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT
# 用 $TMPDIR 存临时文件
echo "临时数据" > "$TMPDIR/data.tmp"
# 脚本退出时自动清理,包括 Ctrl+C
mktemp -d 生成安全的临时目录,不会和其他脚本撞名。trap ... EXIT 保证不管正常退出还是被中断,临时文件都会被清理。
很多人嫌麻烦,直接用 /tmp/script_$$。进程挂了文件就留那儿了,几个月后 /tmp 塞满垃圾。
技巧6:用数组处理多文件
files=(
"/data/logs/app.log"
"/data/logs/error.log"
"/data/logs/access.log"
)
for file in "${files[@]}"; do
if [[ ! -f "$file" ]]; then
log_error "文件不存在: $file"
continue
fi
process_file "$file"
done
用数组比 ls | grep 安全得多。文件名有空格、有特殊字符都不会出问题。
技巧7:网络请求加超时
curl --connect-timeout 10 --max-time 30 \
-sSf "https://api.example.com/data" \
-o /tmp/response.json
--connect-timeout 10:10秒连不上就放弃。
--max-time 30:总共最多跑30秒。
-sSf:静默模式,出错时显示错误信息,失败时返回非零状态码。
没有超时的网络请求是最常见的脚本卡死原因。
技巧8:锁文件防并发
同一个脚本被 cron 同时触发两次,后果可能很严重。加个锁:
LOCK_FILE="/var/run/my_script.lock"
if [[ -e "$LOCK_FILE" ]]; then
pid=$(cat "$LOCK_FILE")
if kill -0 "$pid" 2>/dev/null; then
log_error "脚本已在运行(PID: $pid)"
exit 1
fi
log_warning "发现过期锁文件,已清理"
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
检查锁文件是否存在,如果存在看对应的进程还在不在。进程没了说明是过期锁,清理掉。
技巧9:优雅的信号处理
cleanup() {
log_info "收到退出信号,正在清理..."
rm -f "$TMPDIR"/*
rm -f "$LOCK_FILE"
exit 0
}
trap cleanup INT TERM HUP
用户按 Ctrl+C、系统发 SIGTERM、SSH断开触发 SIGHUP——这些信号都能被捕获。
在清理函数里释放资源、删除临时文件、关闭连接。这样即使脚本被强制中断,也不会留下烂摊子。
技巧10:用 cron 配合日志轮转
# crontab -e
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
简单任务用 cron 就够了。但别忘了日志轮转,不然 /var/log/backup.log 会越来越大。
配合 logrotate:
# /etc/logrotate.d/backup
/var/log/backup.log {
daily
rotate 7
compress
missingok
notifempty
}
每天轮转,保留7天,自动压缩旧日志。
一个完整示例
把这些技巧串起来,一个生产可用的备份脚本长这样:
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
SCRIPT_NAME="backup"
LOG_FILE="/var/log/${SCRIPT_NAME}.log"
LOCK_FILE="/var/run/${SCRIPT_NAME}.lock"
TMPDIR=$(mktemp -d /tmp/${SCRIPT_NAME}.XXXXXX)
log_info() { echo "[$(date '+%F %T')] INFO: $*" | tee -a "$LOG_FILE"; }
log_error() { echo "[$(date '+%F %T')] ERROR: $*" >&2 | tee -a "$LOG_FILE"; }
cleanup() {
rm -rf "$TMPDIR"
rm -f "$LOCK_FILE"
}
trap cleanup EXIT INT TERM
[[ -e "$LOCK_FILE" ]] && { log_error "已有实例运行"; exit 1; }
echo $$ > "$LOCK_FILE"
BACKUP_DIR="/backup/db"
mkdir -p "$BACKUP_DIR"
mysqldump -u user -p'pass' mydb > "$TMPDIR/db.sql"
gzip "$TMPDIR/db.sql"
mv "$TMPDIR/db.sql.gz" "$BACKUP_DIR/"
find "$BACKUP_DIR" -name "*.gz" -mtime +7 -delete
log_info "备份完成"
看着不复杂,但该有的都有了:错误处理、日志、临时文件管理、锁机制、信号处理、自动清理。
最后说两句
Shell脚本不是什么高深技术,但它真的是日常效率的放大器。
写好一个脚本,省下的时间够喝好几杯咖啡。写不好一个脚本,半夜被叫醒排查问题的时候,你会想砸键盘。
从今天开始,把你最常重复的那几个操作写成脚本。用上面对的错误处理和日志,你会发现——原来脚本可以这么省心。
你的日常重复操作是什么?写个脚本试试,跑通了就来分享一下。