Shell 脚本进阶
大约 10 分钟约 3122 字
Shell 脚本进阶
简介
Shell 脚本是 Linux/Unix 系统运维自动化的核心工具,熟练掌握 Shell 脚本的高级特性对于 DevOps 工程师至关重要。本文将深入讲解 Shell 脚本中的条件判断与循环控制、函数定义与模块化编程、文本处理三剑客 awk/sed/grep 的高级用法、定时任务管理以及脚本调试技巧,帮助运维人员编写健壮、高效的自动化脚本。
特点
条件判断与循环控制
Shell 脚本提供了多种条件判断和循环结构,掌握它们的高级用法是编写复杂脚本的基础。
#!/bin/bash
# 高级条件判断示例
set -euo pipefail # 严格模式:遇错退出、未定义变量报错、管道错误传播
# 多条件判断
check_service() {
local service_name="$1"
local port="$2"
# 检查服务是否运行
if systemctl is-active --quiet "$service_name"; then
echo "[OK] $service_name 正在运行"
elif systemctl is-failed --quiet "$service_name"; then
echo "[CRITICAL] $service_name 已失败"
return 2
else
echo "[WARN] $service_name 未运行"
return 1
fi
# 检查端口是否监听
if ss -tlnp | grep -q ":${port} "; then
echo "[OK] 端口 $port 正在监听"
else
echo "[CRITICAL] 端口 $port 未监听"
return 3
fi
}
# 使用 case 语句实现菜单选择
manage_application() {
local action="$1"
case "$action" in
start)
echo "启动应用..."
docker compose up -d
;;
stop)
echo "停止应用..."
docker compose down --timeout 60
;;
restart)
echo "重启应用..."
docker compose restart
;;
status)
docker compose ps
;;
logs)
docker compose logs -f --tail=100 "${2:-}"
;;
*)
echo "用法: $0 {start|stop|restart|status|logs} [service]"
return 1
;;
esac
}#!/bin/bash
# 高级循环和数组操作
# 数组操作
declare -a SERVICES=("nginx" "postgres" "redis" "webapp" "worker")
declare -A SERVICE_PORTS=(
["nginx"]=80
["postgres"]=5432
["redis"]=6379
["webapp"]=8080
["worker"]=0
)
# 批量检查服务状态
echo "========== 服务健康检查 =========="
for service in "${SERVICES[@]}"; do
port="${SERVICE_PORTS[$service]}"
if [ "$port" -eq 0 ]; then
echo "[SKIP] $service (无端口监听)"
continue
fi
# 带超时的端口检查
if timeout 3 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
echo "[OK] $service - 端口 $port 正常"
else
echo "[FAIL] $service - 端口 $port 异常"
FAILED_SERVICES+=("$service")
fi
done
# while 循环读取文件
echo ""
echo "========== 处理配置列表 =========="
while IFS='|' read -r name env replicas image; do
# 跳过注释和空行
[[ "$name" =~ ^#.*$ ]] && continue
[[ -z "$name" ]] && continue
echo "部署: name=$name, env=$env, replicas=$replicas, image=$image"
kubectl scale deployment "$name" --replicas="$replicas" -n "$env"
done < deployment_list.txt
# C 风格 for 循环
echo ""
echo "========== 数据库分片初始化 =========="
for i in $(seq 0 7); do
db_name="shard_${i}"
echo "创建数据库: $db_name"
psql -h localhost -U admin -c "CREATE DATABASE $db_name;" 2>/dev/null || true
done函数与模块化编程
将脚本功能封装为函数和模块,提高代码复用性和可维护性。
#!/bin/bash
# lib/utils.sh - 通用工具函数库
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 日志函数
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
INFO) echo -e "${GREEN}[INFO]${NC} [$timestamp] $message" ;;
WARN) echo -e "${YELLOW}[WARN]${NC} [$timestamp] $message" ;;
ERROR) echo -e "${RED}[ERROR]${NC} [$timestamp] $message" ;;
DEBUG) [[ "${DEBUG:-false}" == "true" ]] && echo -e "${BLUE}[DEBUG]${NC} [$timestamp] $message" ;;
esac
}
# 重试函数
retry() {
local max_attempts="$1"
local delay="$2"
shift 2
local command=("$@")
local attempt=1
while [ $attempt -le $max_attempts ]; do
log INFO "尝试第 $attempt/$max_attempts 次执行: ${command[*]}"
if "${command[@]}"; then
log INFO "命令执行成功"
return 0
fi
log WARN "命令执行失败,${delay}秒后重试..."
sleep "$delay"
attempt=$((attempt + 1))
done
log ERROR "命令在 $max_attempts 次尝试后仍然失败"
return 1
}
# 锁文件机制(防止脚本重复执行)
acquire_lock() {
local lock_file="/tmp/${1}.lock"
if [ -f "$lock_file" ]; then
local pid
pid=$(cat "$lock_file")
if kill -0 "$pid" 2>/dev/null; then
log ERROR "脚本已在运行 (PID: $pid)"
exit 1
fi
log WARN "发现过期锁文件,清理中..."
rm -f "$lock_file"
fi
echo $$ > "$lock_file"
trap 'rm -f "$lock_file"; exit' EXIT INT TERM
}
# 发送通知
notify() {
local title="$1"
local message="$2"
local webhook_url="${SLACK_WEBHOOK_URL:-}"
if [ -n "$webhook_url" ]; then
curl -s -X POST "$webhook_url" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"[$(hostname)] $title: $message\"}" > /dev/null
fi
}#!/bin/bash
# 主脚本:引用函数库
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 加载函数库
source "${SCRIPT_DIR}/lib/utils.sh"
# 使用函数库
acquire_lock "db-backup"
log INFO "开始数据库备份流程"
notify "备份开始" "数据库备份任务已启动"
# 带重试的数据库备份
if retry 3 10 pg_dump -h localhost -U backup_user -d production > "/backup/db_$(date +%Y%m%d_%H%M%S).sql"; then
log INFO "数据库备份完成"
notify "备份成功" "数据库备份已完成"
else
log ERROR "数据库备份失败"
notify "备份失败" "数据库备份失败,请立即检查"
exit 1
fi文本处理 awk/sed
awk 和 sed 是 Shell 文本处理的核心工具,掌握高级用法能够高效处理日志文件和数据转换。
# AWK 高级文本处理
# 1. 分析 Nginx 访问日志:统计每个 IP 的请求次数和流量
awk '
BEGIN {
print "========== Nginx 访问日志分析 =========="
printf "%-18s %10s %15s %15s\n", "IP 地址", "请求次数", "总流量(B)", "平均响应时间"
printf "%-18s %10s %15s %15s\n", "--------", "--------", "----------", "------------"
}
{
ip = $1
# 提取响应大小(第10列)
size = $10
# 提取响应时间(需要在 log_format 中配置)
# 假设格式为: $request_time
gsub(/"/, "", $0)
requests[ip]++
traffic[ip] += size
count++
total_traffic += size
}
END {
# 按请求次数排序输出前 20
n = asorti(requests, sorted_ips)
for (i = 1; i <= n; i++) {
ip = sorted_ips[i]
avg_time = 0
printf "%-18s %10d %15d %15.3f\n", ip, requests[ip], traffic[ip], avg_time
if (i >= 20) break
}
printf "\n总请求数: %d, 总流量: %.2f MB\n", count, total_traffic/1024/1024
}
' /var/log/nginx/access.log
# 2. 使用 awk 处理 CSV 数据并生成报告
awk -F',' '
BEGIN {
OFS=","
print "日期,服务名,平均响应时间(ms),错误率(%),请求总数"
}
NR > 1 {
service = $2
date = $1
resp_time = $3
status = $4
total[service, date]++
resp_sum[service, date] += resp_time
if (status >= 400) errors[service, date]++
}
END {
for (key in total) {
split(key, parts, SUBSEP)
svc = parts[1]
dt = parts[2]
avg_resp = resp_sum[key] / total[key]
error_rate = (errors[key] + 0) / total[key] * 100
printf "%s,%s,%.2f,%.2f,%d\n", dt, svc, avg_resp, error_rate, total[key]
}
}
' metrics.csv > report.csv# SED 高级文本替换和处理
# 1. 批量修改配置文件
# 修改所有 YAML 文件中的镜像版本
find ./manifests -name "*.yaml" -exec sed -i \
-e 's|image: myapp:.*|image: myapp:v2.1.0|g' \
-e 's|image: backend:.*|image: backend:v2.1.0|g' \
{} +
# 2. 删除配置文件中的空行和注释
sed -i -e '/^$/d' -e '/^[[:space:]]*#/d' /etc/myapp/config.conf
# 3. 在特定行后插入内容
# 在 ServerName 后面添加配置
sed -i '/ServerName/a\ ServerAlias www.example.com' /etc/apache2/sites-enabled/app.conf
# 4. 使用 sed 处理日志文件
# 提取特定时间段的错误日志
sed -n '/2024-01-15 10:00/,/2024-01-15 11:00/p' /var/log/app/error.log | \
grep "ERROR" | \
sed 's/\[ERROR\] /** ERROR ** /' | \
sort | uniq -c | sort -rn | head -20
# 5. 多行模式:替换 XML/HTML 标签内容
sed -i ':a;N;$!ba;s|<version>.*<\/version>|<version>2.1.0</version>|g' pom.xml定时任务管理
# Crontab 配置
# 编辑当前用户的定时任务
crontab -e
# 常用定时任务配置
# ┌──────── 分钟 (0-59)
# │ ┌────── 小时 (0-23)
# │ │ ┌──── 日 (1-31)
# │ │ │ ┌── 月 (1-12)
# │ │ │ │ ┌ 星期 (0-7, 0和7都是周日)
# │ │ │ │ │
# * * * * * command
# 每天凌晨 2 点执行数据库备份
0 2 * * * /opt/scripts/backup-db.sh >> /var/log/backup.log 2>&1
# 每 5 分钟检查服务健康状态
*/5 * * * * /opt/scripts/health-check.sh
# 每周一凌晨 3 点清理日志
0 3 * * 1 /opt/scripts/clean-logs.sh
# 每月 1 号凌晨 4 点执行全量备份
0 4 1 * * /opt/scripts/full-backup.sh
# 工作日每天早上 9 点发送报告
0 9 * * 1-5 /opt/scripts/daily-report.sh#!/bin/bash
# /opt/scripts/backup-db.sh - 数据库定时备份脚本
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/utils.sh"
acquire_lock "db-backup"
BACKUP_DIR="/data/backups/postgres"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/postgres_${DATE}.sql.gz"
log INFO "开始数据库备份: $BACKUP_FILE"
# 执行备份
pg_dump -h localhost -U backup_user -d production | gzip > "$BACKUP_FILE"
# 计算文件大小
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log INFO "备份完成,文件大小: $SIZE"
# 上传到远程存储
if aws s3 cp "$BACKUP_FILE" "s3://my-backups/postgres/$(date +%Y/%m/)/" --storage-class STANDARD_IA; then
log INFO "备份已上传到 S3"
else
log ERROR "备份上传失败"
fi
# 清理过期备份
log INFO "清理 ${RETENTION_DAYS} 天前的本地备份"
find "$BACKUP_DIR" -name "postgres_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
log INFO "备份流程完成"脚本调试技巧
# 调试方法 1:使用 bash -x 跟踪执行
bash -x /opt/scripts/deploy.sh
# 调试方法 2:在脚本中启用调试
#!/bin/bash
# 在特定区域启用调试
set -x # 开启调试
# ... 需要调试的代码 ...
set +x # 关闭调试
# 调试方法 3:使用 trap 捕获错误
#!/bin/bash
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "脚本异常退出,退出码: $exit_code"
echo "错误发生在第 $LINENO 行"
# 发送告警
notify "脚本异常" "deploy.sh 在第 $LINENO 行异常退出"
fi
# 清理临时文件
rm -f /tmp/deploy_*.tmp
}
trap cleanup EXIT
# 调试方法 4:使用 BASH_XTRACEFD 输出调试信息到文件
exec 3>/tmp/script_debug.log
BASH_XTRACEFD=3
set -x
# 调试方法 5:语法检查
bash -n /opt/scripts/deploy.sh && echo "语法正确" || echo "语法错误"
# 调试方法 6:使用 shellcheck 静态分析
shellcheck --severity=warning /opt/scripts/deploy.sh优点
缺点
总结
Shell 脚本作为 Linux 运维自动化的基石,通过条件判断、循环控制、函数封装等高级特性,结合 awk/sed 等强大的文本处理工具,能够高效地完成日志分析、配置管理、定时备份等运维任务。虽然 Shell 在复杂系统构建方面不如 Python 等现代语言,但在快速自动化、管道组合和系统管理场景中仍然具有不可替代的优势。掌握 set -euo pipefail 严格模式、trap 错误捕获和 shellcheck 静态分析等最佳实践,能够显著提升 Shell 脚本的健壮性和可维护性。
关键知识点
- DevOps 主题的核心是让交付更快、更稳、更可审计。
- 自动化不是把命令脚本化,而是把失败、回滚、权限和观测一起设计进去。
- 生产链路必须明确制品、环境、凭据、配置和责任边界。
项目落地视角
- 把流水线拆成构建、测试、制品、部署、验证和回滚几个阶段。
- 为关键步骤补齐日志、指标、通知和人工兜底点。
- 定期演练扩容、回滚、故障注入和灾备切换。
常见误区
- 只关注部署成功,不关注失败恢复和审计追踪。
- 把环境差异藏在临时脚本或人工操作里。
- 上线频率高了以后,没有标准化制品和配置管理。
进阶路线
- 继续补齐 GitOps、可观测性、平台工程和成本治理。
- 把主题和应用架构、安全、权限、备份恢复联动起来理解。
- 形成团队级平台能力,而不是每个项目重复造轮子。
适用场景
- 当你准备把《Shell 脚本进阶》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合构建自动化交付、基础设施治理、监控告警和生产发布体系。
- 当团队规模扩大、发布频率提升或环境变多时,这类主题会显著影响交付效率。
落地建议
- 所有自动化流程尽量做到幂等、可审计、可回滚。
- 把制品、变量、凭据和执行权限分层管理。
- 定期演练扩容、回滚、密钥轮换和灾备恢复。
排错清单
- 先定位失败发生在代码、构建、制品、环境还是权限层。
- 检查流水线变量、凭据、镜像标签和目标环境配置是否一致。
- 如果问题偶发,重点看并发发布、资源争抢和外部依赖抖动。
复盘问题
- 如果把《Shell 脚本进阶》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Shell 脚本进阶》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Shell 脚本进阶》最大的收益和代价分别是什么?
