🔧 Linux运维实战 - Shell脚本编写最佳实践
在企业IT运维中,Shell脚本是自动化部署、日志清理、系统监控的基石。然而,很多运维同学写的脚本“能跑就行”,忽视了健壮性、可维护性和安全性。今天,我们结合真实场景,分享几个经过生产环境验证的Shell脚本编写最佳实践。
一、脚本头部:规范与安全
每个脚本都应包含明确的元信息,并启用安全选项。
#!/bin/bash
#——————————————————————
# 脚本名称: cleanup_logs.sh
# 功能: 清理指定目录下超过7天的日志文件
# 作者: 运维组
# 版本: 1.0
# 更新日期: 2025-01-15
#——————————————————————
# 启用严格模式
set -euo pipefail
关键说明:
• set -e:遇到错误立即退出,避免连锁故障
• set -u:使用未定义变量时报错,防止拼写错误
• set -o pipefail:管道中任一命令失败,整体返回非0
• #!/bin/bash而非 /bin/sh,避免不同发行版差异
二、变量与参数:防御式编程
1. 变量赋值时**始终使用双引号**
# 错误写法:当$file包含空格时会分裂成多个参数
rm -rf $file
# 正确写法
rm -rf "$file"
2. 参数检查要有**明确反馈**
if [ $# -ne 2 ]; then
echo "用法: $0 <日志目录> <保留天数>"
exit 1
fi
LOG_DIR="$1"
RETENTION_DAYS="$2"
3. 使用 **readonly** 保护关键变量
readonly MAX_BACKUP_SIZE=1024 # 单位MB
readonly CONFIG_FILE="/etc/backup.conf"
三、函数封装:模块化与复用
避免“面条式脚本”,将逻辑拆分为职责单一的函数。
# 函数:检查目录是否存在且可写
check_directory() {
local dir="$1"
if [ ! -d "$dir" ]; then
echo "错误:目录 $dir 不存在" >&2
return 1
fi
if [ ! -w "$dir" ]; then
echo "错误:目录 $dir 不可写" >&2
return 1
fi
return 0
}
# 函数:安全删除文件(记录日志)
safe_remove() {
local file="$1"
if [ -f "$file" ]; then
rm -f "$file" && echo "$(date) 已删除: $file" >> /var/log/cleanup.log
fi
}
# 主逻辑
main() {
check_directory "$LOG_DIR" || exit 1
find "$LOG_DIR" -type f -name "*.log" -mtime +"$RETENTION_DAYS" | while read -r f; do
safe_remove "$f"
done
}
main "$@"
四、错误处理:日志与返回值
不要默默吞掉错误,给运维人员清晰的排查线索。
1. 统一错误输出到 stderr
log_error() {
echo "$(date) [ERROR] $*" >&2
}
log_info() {
echo "$(date) [INFO] $*"
}
2. 使用 trap 捕获异常信号
cleanup() {
echo "收到中断信号,清理临时文件..."
rm -rf /tmp/backup_$$
exit 1
}
trap cleanup INT TERM
3. 命令执行结果检查
if ! tar czf backup.tar.gz /data; then
log_error "备份打包失败"
exit 2
fi
五、性能与安全:避免常见陷阱
⚠️ 避免使用 `ls` 解析文件名
# 错误:文件名含换行符会崩溃
for file in $(ls *.log); do ...
# 正确:使用 find 或通配符
for file in *.log; do
[ -e "$file" ] || continue
echo "处理: $file"
done
🔒 临时文件安全
# 使用 mktemp 创建唯一临时文件
TEMP_FILE=$(mktemp /tmp/script_XXXXXX)
trap "rm -f $TEMP_FILE" EXIT
📊 避免无意义的管道链
# 低效:cat + grep 多此一举
cat /var/log/syslog | grep "error"
# 高效:直接 grep
grep "error" /var/log/syslog
六、实战案例:日志归档脚本
完整示例,展示上述实践的综合应用:
#!/bin/bash
set -euo pipefail
readonly APP_NAME="webapp"
readonly LOG_DIR="/var/log/$APP_NAME"
readonly ARCHIVE_DIR="/backup/logs"
readonly RETENTION_DAYS=30
log_info() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $*"; }
log_error() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $*" >&2; }
archive_logs() {
local date_str=$(date +%Y%m%d)
local archive_file="${ARCHIVE_DIR}/${APP_NAME}_${date_str}.tar.gz"
if [ ! -d "$ARCHIVE_DIR" ]; then
mkdir -p "$ARCHIVE_DIR" || { log_error "无法创建归档目录"; exit 1; }
fi
# 归档前一天的日志(避免正在写入的文件)
find "$LOG_DIR" -maxdepth 1 -name "*.log" -mtime +1 -print0 | \
xargs -0 tar czf "$archive_file" || {
log_error "归档失败"; return 1
}
# 删除已归档的原始文件
find "$LOG_DIR" -maxdepth 1 -name "*.log" -mtime +1 -delete
log_info "归档完成: $archive_file"
}
cleanup_old_archives() {
find "$ARCHIVE_DIR" -name "*.tar.gz" -mtime +"$RETENTION_DAYS" -delete
log_info "已清理 $RETENTION_DAYS 天前的归档"
}
main() {
log_info "开始日志归档任务"
archive_logs
cleanup_old_archives
log_info "任务完成"
}
main "$@"
总结
优秀的Shell脚本不是“能跑就行”,而是可读、可维护、可调试。牢记三条原则:变量加引号、函数拆逻辑、错误有反馈。掌握这些最佳实践,你的脚本将真正成为运维利器,而非定时炸弹。