说实话,刚开始学 Shell 的时候,我和你们一样,觉得这玩意儿不就是 ls、cd、grep 那一套吗?直到有一天,生产环境的磁盘被日志打满了,而我的手机正好没电,没法连进去手动删,那种绝望感我至今记得。从那以后,我才真正意识到:Shell 不只是命令的堆砌,它是运维人员的“救命稻草”,也是系统自动化的“神经末梢”。
今天这篇长文,我想放下那些教科书式的定义,像老朋友聊天一样,带你从头到尾把 Shell 运维脚本这条路走通。我们会聊基础、聊实战、聊坑、聊优化,甚至聊怎么让老板觉得你很有价值。准备好了吗?咱们开始。
第一章:别急着写脚本,先把“地基”打牢
很多人写 Shell 脚本出问题,不是因为逻辑错了,而是因为基础语法理解偏差。别嫌我啰嗦,这些细节决定了你的脚本是“能跑”还是“健壮”。
1.1 Shebang 不是摆设
#!/bin/bash
这一行必须写在第一行。它告诉操作系统:“请用 /bin/bash 来解释执行这个脚本”。如果你写成 #!/bin/sh,在某些 Linux 发行版(如 Ubuntu)中,sh 可能指向 dash,而 dash 不支持 Bash 的一些高级特性(比如数组、[[ ]] 等),这会导致脚本报错,而且错误信息非常晦涩。
建议:除非你明确需要兼容 POSIX 标准,否则一律用 #!/bin/bash。
1.2 变量赋值:空格是“毒药”
# 正确
name="zhangsan"
age=25
# 错误!这会报错:age=25: command not found
name = "zhangsan"
在 Shell 中,= 两边不能有空格。这是新手最常踩的坑。赋值时,如果值包含空格,必须用引号括起来。
1.3 条件判断:[ 和 [[ ]] 的区别
# 旧式写法,容易出错
if [ "$name" == "admin" ]; then
echo "Hello admin"
fi
# 新式写法,更安全,支持正则
if [[ "$name" == "admin" ]]; then
echo "Hello admin"
fi
[ ]是 POSIX 标准,功能有限,需要小心转义。[[ ]]是 Bash 扩展,更强大,支持逻辑运算符&&、||,支持正则匹配,推荐在 Bash 脚本中使用。
1.4 引号的秘密
- 双引号
":允许变量替换,但不允许命令替换(除非用$())。 - 单引号
':完全字面量,什么也不解释。 - 反引号
` `或$():命令替换。
echo "Today is $(date)" # 推荐:可读性好,可嵌套
echo "Today is `date`" # 不推荐:反引号在视觉上容易混淆
第二章:从“手工侠”到“自动化战士”——日志清理实战
回到我开头说的那件事:磁盘满了。如果当时我有个自动化脚本,情况会完全不同。
2.1 需求分析
我们要清理的是 Nginx 访问日志,保留最近 7 天的日志,其他的删除。同时,要确保脚本不会误删其他目录。
2.2 基础版脚本(充满坑)
#!/bin/bash
cd /var/log/nginx
rm -f *.log.*
这个脚本有重大隐患:
rm -f *.log.*会删除所有带*的日志,包括可能正在写入的日志。- 没有日志记录,你不知道谁删了什么。
- 没有备份机制,误删无法恢复。
2.3 进阶版:安全清理脚本
#!/bin/bash
# 配置项
LOG_DIR="/var/log/nginx"
RETENTION_DAYS=7
LOG_FILE="/var/log/cleanup.log"
# 记录开始时间
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始清理 $LOG_DIR" >> "$LOG_FILE"
# 检查目录是否存在
if [ ! -d "$LOG_DIR" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 错误:目录 $LOG_DIR 不存在" >> "$LOG_FILE"
exit 1
fi
# 查找并删除超过保留期的日志
# 使用 find 命令,-mtime +7 表示修改时间在 7 天以前
find "$LOG_DIR" -name "*.log.*" -type f -mtime +${RETENTION_DAYS} | while read -r file; do
# 备份前重命名(可选,用于防止误删)
backup_file="${LOG_DIR}/$(basename "$file").bak.$(date +%s)"
mv "$file" "$backup_file"
# 删除备份
rm -f "$backup_file"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 已删除: $file" >> "$LOG_FILE"
done
# 检查磁盘空间
df -h "$LOG_DIR" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 清理完成" >> "$LOG_FILE"
关键点解析:
find ... | while read -r:管道传递给 while 循环,-r防止反斜杠转义。date +%s:时间戳,确保备份文件名唯一。>> "$LOG_FILE":所有操作都有日志记录,方便审计。
2.4 常见坑点:通配符与空目录
如果 /var/log/nginx 下没有匹配的文件,find 不会返回任何东西,脚本正常结束。但如果你用 ls *.log.*,在没有匹配文件时会报错。所以永远优先使用 find,而不是 ls 配合通配符。
第三章:备份的艺术——不只是复制粘贴
备份是运维的底线。没有备份的系统,就像没有保险的汽车。
3.1 全量备份 vs 增量备份
- 全量备份:每次都复制所有数据。简单,但耗空间、耗时间。
- 增量备份:只复制自上次备份以来变化的数据。复杂,但高效。
对于小规模的日志和配置文件,全量备份足够了,因为逻辑简单,不容易出错。
3.2 脚本:自动备份 Nginx 配置和日志
#!/bin/bash
# 配置项
BACKUP_DIR="/data/backup/nginx"
SOURCE_DIR="/etc/nginx"
LOG_SOURCE_DIR="/var/log/nginx"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/nginx_config_${DATE}.tar.gz"
LOG_BACKUP_FILE="${BACKUP_DIR}/nginx_logs_${DATE}.tar.gz"
# 创建备份目录(如果不存在)
mkdir -p "$BACKUP_DIR"
# 备份配置文件
tar -czf "$BACKUP_FILE" -C /etc nginx
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 配置文件备份完成: $BACKUP_FILE"
# 备份最近 7 天的日志
tar -czf "$LOG_BACKUP_FILE" -C /var/log nginx/access.log.* $(find /var/log/nginx -name "*.log.*" -mtime -7 -exec basename {} \;)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 日志备份完成: $LOG_BACKUP_FILE"
# 删除旧备份
find "$BACKUP_DIR" -name "*.tar.gz" -type f -mtime +${RETENTION_DAYS} -delete
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 旧备份清理完成"
注意:备份日志时,我们只备份最近 7 天的,避免压缩文件过大。
3.3 备份验证:备份了不等于能恢复
很多运维人员只备份,不验证。这是大忌。
# 在脚本末尾添加验证步骤
if tar -tzf "$BACKUP_FILE" > /dev/null 2>&1; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 备份文件验证通过"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 警告:备份文件损坏,请检查"
# 发送告警
send_alert "Nginx config backup failed!"
fi
第四章:监控报警——让问题在爆发前被发现
监控不是事后诸葛亮,而是事前预警。
4.1 监控磁盘空间
#!/bin/bash
THRESHOLD=90 # 磁盘使用率超过 90% 时报警
ALERT_EMAIL="admin@example.com"
# 获取根分区使用率
USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "警告:根分区使用率 ${USAGE}%,已超过 ${THRESHOLD}%" | mail -s "磁盘空间告警" "$ALERT_EMAIL"
fi
4.2 监控进程存活
#!/bin/bash
PROCESS_NAME="nginx"
if ! pgrep -x "$PROCESS_NAME" > /dev/null; then
# 进程不存在,尝试重启
systemctl restart "$PROCESS_NAME"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $PROCESS_NAME 进程未运行,已尝试重启" >> /var/log/process_monitor.log
# 发送告警
mail -s "$PROCESS_NAME 进程异常" "$ALERT_EMAIL"
fi
4.3 监控 CPU 和内存
#!/bin/bash
# 获取 CPU 使用率(前 5 秒的采样)
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
# 更稳定的方式:使用 /proc/stat
CPU_USAGE=$(awk '/^cpu / {usage=($2+$3+$4)*100/($2+$3+$4+$5+$6+$7+$8); printf "%.1f", usage}' /proc/stat)
THRESHOLD=80
if (( $(echo "$CPU_USAGE > $THRESHOLD" | bc -l) )); then
echo "警告:CPU 使用率 ${CPU_USAGE}%" | mail -s "CPU 告警" "$ALERT_EMAIL"
fi
坑点:top 命令的输出格式在不同系统上可能不同,建议优先使用 /proc/stat 或 mpstat。
4.4 集中告警:封装一个通用报警函数
send_alert() {
local message="$1"
local subject="运维告警: $message"
# 发送到邮件
echo "$message" | mail -s "$subject" "$ALERT_EMAIL"
# 发送到钉钉/企业微信(这里以 curl 为例)
curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text", "text": {"content": "'$subject': $message"}}'
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 告警已发送: $message" >> /var/log/alert.log
}
这样,你只需要调用 send_alert "磁盘空间不足",就能同时发送邮件和钉钉通知,大大简化了代码。
第五章:常见坑点与优化技巧
写脚本容易,写好脚本难。以下是一些我在实战中踩过的坑和总结的技巧。
5.1 坑点一:未处理的变量展开
# 危险:如果 $file 包含空格,脚本会出错
rm $file
# 安全:始终用双引号括起来
rm "$file"
5.2 坑点二:忽略错误码
# 错误:即使 mkdir 失败,脚本也会继续执行
mkdir /data/backup
tar -czf backup.tar.gz /data
# 正确:使用 set -e 或在关键命令后检查返回值
set -e
mkdir /data/backup || exit 1
tar -czf backup.tar.gz /data
set -e 会让脚本在任何一个命令失败时立即退出,避免错误累积。
5.3 坑点三:硬编码路径
不要把所有路径都写在脚本里。把它们提取到配置文件中。
config.sh
LOG_DIR="/var/log/nginx"
BACKUP_DIR="/data/backup"
RETENTION_DAYS=7
cleanup.sh
#!/bin/bash
source config.sh
# 使用 $LOG_DIR 等变量
这样,换一台服务器只需要改配置文件,不用改脚本。
5.4 优化技巧一:使用 set -o pipefail
set -o pipefail
默认情况下,管道命令的错误码只取最后一个命令的。设置 pipefail 后,如果管道中任何一个命令失败,整个管道的退出码就是非零的。
# 示例:grep 找不到匹配项时返回 1,默认会被忽略
echo "hello" | grep "world"
echo $? # 输出 1,但如果没有 pipefail,前面的命令错误可能被忽略
set -o pipefail
echo "hello" | grep "world"
echo $? # 输出 1,正确反映错误
5.5 优化技巧二:日志分级
不要只写一行日志。使用不同的日志级别,方便后续排查。
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1" >> "$LOG_FILE"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" >> "$LOG_FILE"
}
log_warn() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1" >> "$LOG_FILE"
}
5.6 优化技巧三:使用 trap 清理临时文件
TMPFILE=$(mktemp)
trap "rm -f $TMPFILE" EXIT
# 在脚本中使用 $TMPFILE
# 即使脚本异常退出,临时文件也会被自动删除
5.7 优化技巧四:脚本签名与帮助信息
一个好的运维脚本,应该像工具一样易用。添加帮助信息,让用户知道怎么用。
show_help() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -h, --help Show this help message"
echo " -d, --days Retention days (default: 7)"
echo " -v, --verbose Verbose output"
}
# 解析参数
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) show_help; exit 0 ;;
-d|--days) RETENTION_DAYS=$2; shift 2 ;;
-v|--verbose) VERBOSE=true; shift ;;
*) echo "Unknown option: $1"; show_help; exit 1 ;;
esac
done
第六章:一个完整的实战案例——自动化运维脚本框架
让我们把前面学到的东西整合起来,写一个完整的、生产可用的运维脚本框架。
脚本名称: ops_tool.sh
”`bash #!/bin/bash
=============================================================================
脚本名称: ops_tool.sh
功能: 统一的运维操作工具,包含日志清理、备份、监控等功能
作者: 你的昵称
版本: 1.0.0
用法: ./ops_tool.sh [options]
=============================================================================
set -euo pipefail
— 全局配置 —
SCRIPT_NAME=\((basename "\)0”) VERSION=“1.0.0” LOG_FILE=“/var/log/ops_tool.log” CONFIG_FILE=“/etc/ops_tool.conf”
— 函数定义 —
日志记录
log() {
local level=$1
local message=$2
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a "$LOG_FILE"
}
log_info() { log “INFO” “\(1"; } log_error() { log "ERROR" "\)1”; } log_warn() { log “WARN” “$1”; }
发送告警
send_alert() {
local message="$1"
log_warn "发送告警: $message"
# 这里调用之前定义的 send_alert 函数
# 为了演示,我们只打印
echo "ALERT: $message"
}
加载配置
load_config() {
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
log_info "配置文件加载成功: $CONFIG_FILE"
else
log_warn "配置文件不存在,使用默认值"
fi
}
显示帮助
show_help() {
cat << EOF
\(SCRIPT_NAME v\)VERSION
Usage: $SCRIPT_NAME
Commands: clean 清理日志 backup 备份配置和日志 monitor 执行监控检查 help 显示此帮助信息
Options: -h, –help Show this help message -c, –config 指定
