Shell编程从入门到实战

一、为什么每个开发者都必须掌握Shell?

1.1 真实场景:没有Shell,运维怎么活?

想象这样一个凌晨3点的场景:

生产环境突然告警,50台服务器的磁盘空间同时告急。你坐在电脑前,是选择:

  • 方案A :一台台SSH登录,手动执行df -h,复制粘贴清理命令,预计耗时3小时
  • 方案B:运行一条命令,30秒内完成全集群巡检和自动清理

Shell脚本就是那个"方案B"。

1.2 Shell不可替代的三大理由

对比维度 手动操作 Shell脚本 Python脚本
执行速度 慢(人肉操作) 极快(系统级调用) 中等(需解释器)
环境依赖 几乎无(Bash内置) 需安装Python及库
学习成本 低(但重复劳动) 中(一次学习终身受益) 中高
系统亲和度 - 原生支持 需适配
适用场景 一次性操作 自动化、批量化 复杂业务逻辑

核心结论: Shell是Linux系统的"母语",任何与系统打交道的场景,Shell都是首选。


二、脚本骨架:每个脚本都应该有的标准结构(⭐⭐⭐重点)

2.1 为什么需要标准模板?

先看一个反面教材------新手常写的脚本:

bash 复制代码
#!/bin/bash
a=$1
b=$2
c=$a+$b
echo $c

问题诊断:

  • ❌ 没有错误处理,参数缺失直接报错
  • ❌ 没有注释,3天后自己都不认识
  • ❌ 没有日志,出问题无法追溯
  • ❌ 没有帮助信息,别人不会用

2.2 企业级标准模板(可直接复制使用)

bash 复制代码
#!/bin/bash
#===============================================================================
#
#          FILE: backup_mysql.sh
#
#         USAGE: ./backup_mysql.sh [OPTIONS]
#
#   DESCRIPTION: MySQL数据库备份脚本,支持全量备份和指定库备份
#                特性:自动压缩、过期清理、钉钉告警、执行日志
#
#       OPTIONS: -h 显示帮助信息
#                -d 指定数据库名(默认全库)
#                -p 指定备份路径(默认: /data/backup/mysql)
#                -r 保留天数(默认: 7天)
#
#  REQUIREMENTS: mysqldump >= 5.7, gzip, bc
#
#          BUGS: 暂无已知问题
#
#         NOTES: 1. 建议加入crontab定时执行:0 2 * * * /path/backup_mysql.sh
#                2. 首次运行前请确保备份目录存在且有写权限
#                3. 敏感信息(密码)建议通过环境变量或配置文件传入
#
#        AUTHOR: Your Name <your.email@example.com>
#       VERSION: 1.0.0
#       CREATED: 2024-01-01
#      REVISION: 2024-03-15 增加磁盘空间预检查
#===============================================================================

#-------------------------------------------------------------------------------
# 严格模式设置(这三行是脚本的"安全气囊")
#-------------------------------------------------------------------------------
set -euo pipefail
# -e: 命令失败立即退出,防止错误继续执行导致更大问题
# -u: 使用未定义变量时报错,避免变量名拼写错误
# -o pipefail: 管道中任一命令失败则整体失败,防止"静默失败"

#-------------------------------------------------------------------------------
# 全局变量定义(集中管理,拒绝硬编码)
#-------------------------------------------------------------------------------
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly LOG_DIR="/var/log/shell-scripts"
readonly LOG_FILE="${LOG_DIR}/${SCRIPT_NAME%.sh}.log"
readonly LOCK_FILE="/var/run/${SCRIPT_NAME%.sh}.lock"
readonly BACKUP_DIR="/data/backup/mysql"
readonly DATE=$(date +%Y%m%d_%H%M%S)
readonly RETENTION_DAYS=7

# 颜色定义(用于终端输出,生产环境可关闭)
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color

#-------------------------------------------------------------------------------
# 日志函数(分级日志,便于排查问题)
#-------------------------------------------------------------------------------
# 确保日志目录存在
mkdir -p "$LOG_DIR"

log_info() {
    local message="[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] [$$] $*"
    echo -e "${GREEN}${message}${NC}" | tee -a "$LOG_FILE"
}

log_warn() {
    local message="[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] [$$] $*"
    echo -e "${YELLOW}${message}${NC}" | tee -a "$LOG_FILE"
}

log_error() {
    local message="[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] [$$] $*"
    echo -e "${RED}${message}${NC}" | tee -a "$LOG_FILE"
}

log_debug() {
    # 调试日志,通过环境变量控制是否输出
    if [[ "${DEBUG:-0}" == "1" ]]; then
        local message="[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] [$$] $*"
        echo -e "${BLUE}${message}${NC}" | tee -a "$LOG_FILE"
    fi
}

#-------------------------------------------------------------------------------
# 帮助信息
#-------------------------------------------------------------------------------
usage() {
    cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS]

MySQL数据库备份脚本 - 企业级备份解决方案

Options:
    -h, --help          显示帮助信息
    -d, --database      指定数据库名(默认全库备份)
    -p, --path          指定备份路径(默认: ${BACKUP_DIR})
    -r, --retention     保留天数(默认: ${RETENTION_DAYS}天)
    --debug             开启调试模式,输出详细日志

Examples:
    # 全库备份
    ${SCRIPT_NAME}
    
    # 备份指定数据库
    ${SCRIPT_NAME} -d mydb
    
    # 指定备份路径和保留天数
    ${SCRIPT_NAME} -d mydb -p /tmp/backup -r 3
    
    # 调试模式运行
    DEBUG=1 ${SCRIPT_NAME} -d mydb

Exit Codes:
    0   成功
    1   参数错误
    2   权限不足
    3   依赖缺失
    4   备份失败
EOF
}

#-------------------------------------------------------------------------------
# 参数解析(使用手动解析,兼容长选项)
#-------------------------------------------------------------------------------
parse_args() {
    local database=""
    local backup_path="$BACKUP_DIR"
    local retention="$RETENTION_DAYS"

    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)
                usage
                exit 0
                ;;
            -d|--database)
                if [[ -z "${2:-}" ]]; then
                    log_error "选项 -d 需要参数"
                    exit 1
                fi
                database="$2"
                shift 2
                ;;
            -p|--path)
                if [[ -z "${2:-}" ]]; then
                    log_error "选项 -p 需要参数"
                    exit 1
                fi
                backup_path="$2"
                shift 2
                ;;
            -r|--retention)
                if [[ -z "${2:-}" ]]; then
                    log_error "选项 -r 需要参数"
                    exit 1
                fi
                retention="$2"
                shift 2
                ;;
            --debug)
                DEBUG=1
                shift
                ;;
            *)
                log_error "未知参数: $1"
                usage
                exit 1
                ;;
        esac
    done

    # 参数校验
    if ! [[ "$retention" =~ ^[0-9]+$ ]]; then
        log_error "保留天数必须是正整数: $retention"
        exit 1
    fi

    # 返回解析结果(使用全局变量)
    BACKUP_DATABASE="$database"
    BACKUP_PATH="$backup_path"
    RETENTION="$retention"
}

#-------------------------------------------------------------------------------
# 前置检查(环境、依赖、权限、锁)
#-------------------------------------------------------------------------------
pre_check() {
    log_info "开始前置检查..."
    
    # 检查锁文件(防止重复执行)
    if [[ -f "$LOCK_FILE" ]]; then
        local pid=$(cat "$LOCK_FILE")
        if kill -0 "$pid" 2>/dev/null; then
            log_error "脚本正在运行中 (PID: $pid),请勿重复执行"
            exit 1
        else
            log_warn "发现过期锁文件,已清理"
            rm -f "$LOCK_FILE"
        fi
    fi
    echo $$ > "$LOCK_FILE"
    
    # 检查是否root运行(根据实际需求)
    # if [[ $EUID -ne 0 ]]; then
    #     log_error "请使用root权限运行此脚本"
    #     exit 2
    # fi

    # 检查依赖命令
    local deps=("mysqldump" "gzip" "mysql")
    for cmd in "${deps[@]}"; do
        if ! command -v "$cmd" &> /dev/null; then
            log_error "未找到依赖命令: $cmd,请先安装"
            exit 3
        fi
        log_debug "依赖检查通过: $cmd"
    done

    # 检查磁盘空间(预留10%)
    local available=$(df "$backup_path" | awk 'NR==2{print $4}')
    local required=1048576  # 1GB = 1048576 KB
    if [[ $available -lt $required ]]; then
        log_error "磁盘空间不足,可用: ${available}KB,需要: ${required}KB"
        exit 2
    fi
    log_info "磁盘空间检查通过"

    # 创建备份目录
    mkdir -p "$BACKUP_PATH"
    if [[ ! -w "$BACKUP_PATH" ]]; then
        log_error "备份目录不可写: $BACKUP_PATH"
        exit 2
    fi
    
    log_info "前置检查全部通过"
}

#-------------------------------------------------------------------------------
# 核心功能:执行备份
#-------------------------------------------------------------------------------
do_backup() {
    local database="$1"
    local backup_path="$2"
    local backup_file="${backup_path}/backup_${database:-all}_${DATE}.sql.gz"

    log_info "开始备份,目标文件: $backup_file"

    # 构建mysqldump命令
    local dump_cmd="mysqldump --single-transaction --quick --lock-tables=false"
    dump_cmd+=" --routines --triggers"
    
    if [[ -n "$database" ]]; then
        dump_cmd+=" $database"
    else
        dump_cmd+=" --all-databases"
    fi
    
    log_debug "执行命令: $dump_cmd | gzip > $backup_file"
    
    # 执行备份并压缩
    if ! eval "$dump_cmd" 2>/dev/null | gzip > "$backup_file"; then
        log_error "备份命令执行失败"
        rm -f "$backup_file"  # 清理残次文件
        return 4
    fi

    # 验证备份文件
    if [[ ! -f "$backup_file" || ! -s "$backup_file" ]]; then
        log_error "备份文件不存在或为空"
        return 4
    fi

    local size=$(du -h "$backup_file" | cut -f1)
    log_info "备份成功,文件大小: $size"
    return 0
}

#-------------------------------------------------------------------------------
# 清理过期备份
#-------------------------------------------------------------------------------
cleanup() {
    local backup_path="$1"
    local retention="$2"

    log_info "清理${retention}天前的备份文件..."
    
    local count_before=$(find "$backup_path" -name "backup_*.sql.gz" | wc -l)
    
    find "$backup_path" -name "backup_*.sql.gz" -mtime +"$retention" -delete
    
    local count_after=$(find "$backup_path" -name "backup_*.sql.gz" | wc -l)
    local deleted=$((count_before - count_after))
    
    log_info "清理完成,删除${deleted}个文件,剩余${count_after}个文件"
}

#-------------------------------------------------------------------------------
# 清理函数(脚本退出时执行)
#-------------------------------------------------------------------------------
cleanup_lock() {
    rm -f "$LOCK_FILE"
    log_debug "锁文件已清理"
}

# 注册退出时的清理操作
trap cleanup_lock EXIT

#-------------------------------------------------------------------------------
# 主函数(程序入口)
#-------------------------------------------------------------------------------
main() {
    log_info "========== 脚本开始执行 =========="
    log_info "脚本版本: 1.0.0"
    log_info "执行用户: $(whoami)"
    log_info "工作目录: $(pwd)"
    
    parse_args "$@"
    pre_check
    
    if ! do_backup "$BACKUP_DATABASE" "$BACKUP_PATH"; then
        exit 4
    fi
    
    cleanup "$BACKUP_PATH" "$RETENTION"
    
    log_info "========== 脚本执行完成 =========="
}

# 执行主函数,传入所有参数
main "$@"

模板核心亮点解析:

特性 作用 生产价值
set -euo pipefail 严格错误处理 防止"静默失败",问题早发现
锁文件机制 防止重复执行 避免定时任务重叠导致资源争抢
分级日志 INFO/WARN/ERROR/DEBUG 快速定位问题,支持调试开关
参数校验 类型检查、必填校验 防止非法输入导致不可预期行为
trap EXIT 退出时自动清理 避免锁文件残留导致后续执行失败
退出码规范 0=成功,1-4=不同错误 便于上层系统(如Zabbix)监控

三、核心语法深度解析

3.1 变量与引号:一个空格引发的惨案

实验:验证引号的差异

bash 复制代码
#!/bin/bash

# 创建测试文件
touch "my file.txt"  # 文件名包含空格

FILE="my file.txt"

echo "===== 实验1:无引号 ====="
echo $FILE
# 输出:my file.txt(被拆成两个参数!)
# 实际执行效果等同于:echo my file.txt

echo "===== 实验2:双引号 ====="
echo "$FILE"
# 输出:my file.txt(正确,一个整体)

echo "===== 实验3:单引号 ====="
echo '$FILE'
# 输出:$FILE(原样输出,不解析变量)

echo "===== 实验4:反引号(命令替换)====="
echo "`date`"
# 输出:Wed May 13 12:00:00 CST 2026

echo "===== 实验5:\$() 现代命令替换 ====="
echo "$(date)"
# 输出同上,但可嵌套,推荐!

# 危险操作演示(别在生产环境执行!)
# rm $FILE      # 错误!会尝试删除"my"和"file.txt"两个文件
# rm "$FILE"    # 正确!删除名为"my file.txt"的单个文件

# 清理测试文件
rm -f "my file.txt"

黄金法则:变量永远用双引号包裹!

3.2 条件判断:test、[ ]、[[ ]] 的选择

对比实验:

bash 复制代码
#!/bin/bash

STR="hello world"
NUM=10

echo "===== 实验1:[ ] 需要引号 ====="
# if [ $STR == "hello world" ]; then  # 错误!会被拆成多个单词
#     echo "匹配"
# fi

if [ "$STR" == "hello world" ]; then   # 正确,必须加引号
    echo "[ ] 匹配成功"
fi

echo "===== 实验2:[[ ]] 不需要引号 ====="
if [[ $STR == hello* ]]; then          # 正确,不会单词分割
    echo "[[ ]] 模式匹配成功: $STR"
fi

echo "===== 实验3:[[ ]] 支持正则 ====="
if [[ $STR =~ ^h.*d$ ]]; then
    echo "[[ ]] 正则匹配成功"
fi

echo "===== 实验4:[[ ]] 支持逻辑运算符 ====="
if [[ $NUM -gt 5 && $NUM -lt 15 ]]; then
    echo "NUM在5到15之间"
fi

# [ ] 中不能这样写,必须:
# if [ $NUM -gt 5 ] && [ $NUM -lt 15 ]; then

选择建议:

场景 推荐 原因
简单数字比较 [ ][[ ]] [ ] 更兼容POSIX
字符串匹配、正则 [[ ]] 功能更强大
复杂逻辑组合 [[ ]] && `
需要跨平台(sh/dash) [ ] [[ ]] 是Bash扩展

3.3 循环结构:不同场景的最佳选择

场景对比实验:

bash 复制代码
#!/bin/bash

echo "===== 场景1:遍历文件(注意通配符陷阱)====="
# 错误写法:如果目录为空,会输出 "*.log" 字面量
# for file in *.log; do
#     echo "$file"
# done

# 正确写法:检查文件是否存在
for file in /var/log/*.log; do
    [[ -f "$file" ]] || continue  # 跳过不存在的文件
    echo "处理: $(basename "$file")"
done

echo ""
echo "===== 场景2:C风格循环(需要索引时)====="
arr=("apple" "banana" "cherry")
for ((i=0; i<${#arr[@]}; i++)); do
    echo "[$i] ${arr[$i]}"
done

echo ""
echo "===== 场景3:读取文件每行(while + read)====="
# 正确方式:IFS= 防止去掉首尾空格,-r 防止反斜杠转义
while IFS= read -r line; do
    echo "读取: $line"
done << EOF
    line with leading spaces
    normal line
EOF

echo ""
echo "===== 场景4:带索引的数组遍历 ====="
for i in "${!arr[@]}"; do
    echo "索引 $i 的值: ${arr[$i]}"
done

3.4 数组操作:Shell的"高级数据结构"

bash 复制代码
#!/bin/bash

# 普通数组(索引数组)
echo "===== 普通数组 ====="
SERVERS=("192.168.1.10" "192.168.1.11" "192.168.1.12")
echo "原始数组: ${SERVERS[@]}"
echo "数组长度: ${#SERVERS[@]}"

# 添加元素
SERVERS+=("192.168.1.13")
echo "添加后: ${SERVERS[@]}"

# 数组切片
echo "前两个: ${SERVERS[@]:0:2}"

# 删除元素
unset SERVERS[1]
echo "删除索引1后: ${SERVERS[@]}"

echo ""
echo "===== 关联数组(Bash 4.0+,类似Map/字典)====="
declare -A USER_MAP
USER_MAP["admin"]="管理员"
USER_MAP["guest"]="访客"
USER_MAP["root"]="超级管理员"

echo "admin的角色: ${USER_MAP["admin"]}"
echo "所有键: ${!USER_MAP[@]}"
echo "所有值: ${USER_MAP[@]}"

echo ""
echo "===== 遍历关联数组 ====="
for key in "${!USER_MAP[@]}"; do
    printf "%-10s -> %s\n" "$key" "${USER_MAP[$key]}"
done

四、大数据实战脚本

脚本1:Hadoop集群一键启停脚本

bash 复制代码
#!/bin/bash
#===============================================================================
# Hadoop集群管理脚本
# 功能:一键启动/停止HDFS、YARN、HistoryServer
# 配置:修改HADOOP_HOME和主机名即可使用
#===============================================================================
set -euo pipefail

# 配置区(根据实际环境修改)
readonly HADOOP_HOME="/opt/module/hadoop-3.1.3"
readonly NAMENODE_HOST="hadoop101"
readonly RESOURCEMANAGER_HOST="hadoop102"

# 颜色输出
readonly GREEN='\033[0;32m'
readonly RED='\033[0;31m'
readonly NC='\033[0m'

log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }

usage() {
    echo "Usage: $0 {start|stop|status}"
    exit 1
}

# 检查SSH免密是否配置
check_ssh() {
    local host="$1"
    if ! ssh -o ConnectTimeout=5 "$host" "echo OK" &>/dev/null; then
        log_error "无法连接到 $host,请检查SSH免密配置"
        exit 1
    fi
}

# 启动集群
start_cluster() {
    log_info "========== 启动 Hadoop 集群 =========="
    
    check_ssh "$NAMENODE_HOST"
    check_ssh "$RESOURCEMANAGER_HOST"
    
    log_info "[1/3] 启动 HDFS..."
    ssh "$NAMENODE_HOST" "${HADOOP_HOME}/sbin/start-dfs.sh"
    
    log_info "[2/3] 启动 YARN..."
    ssh "$RESOURCEMANAGER_HOST" "${HADOOP_HOME}/sbin/start-yarn.sh"
    
    log_info "[3/3] 启动 HistoryServer..."
    ssh "$NAMENODE_HOST" "${HADOOP_HOME}/bin/mapred --daemon start historyserver"
    
    log_info "========== 启动完成 =========="
}

# 停止集群
stop_cluster() {
    log_info "========== 停止 Hadoop 集群 =========="
    
    log_info "[1/3] 停止 HistoryServer..."
    ssh "$NAMENODE_HOST" "${HADOOP_HOME}/bin/mapred --daemon stop historyserver" 2>/dev/null || true
    
    log_info "[2/3] 停止 YARN..."
    ssh "$RESOURCEMANAGER_HOST" "${HADOOP_HOME}/sbin/stop-yarn.sh" 2>/dev/null || true
    
    log_info "[3/3] 停止 HDFS..."
    ssh "$NAMENODE_HOST" "${HADOOP_HOME}/sbin/stop-dfs.sh" 2>/dev/null || true
    
    log_info "========== 停止完成 =========="
}

# 查看集群状态
status_cluster() {
    log_info "========== 集群进程状态 =========="
    
    local hosts=("$NAMENODE_HOST" "$RESOURCEMANAGER_HOST")
    for host in "${hosts[@]}"; do
        echo ""
        echo "----- $host -----"
        ssh "$host" "jps" 2>/dev/null || echo "无法连接"
    done
}

# 主程序
case "${1:-}" in
    start)
        start_cluster
        ;;
    stop)
        stop_cluster
        ;;
    status)
        status_cluster
        ;;
    *)
        usage
        ;;
esac

使用方式:

bash 复制代码
chmod +x hadoop-cluster.sh
./hadoop-cluster.sh start    # 启动集群
./hadoop-cluster.sh stop     # 停止集群
./hadoop-cluster.sh status   # 查看状态

脚本2:集群分发脚本(xsync升级版)

bash 复制代码
#!/bin/bash
#===============================================================================
# 集群文件分发脚本
# 功能:将本地文件/目录同步到集群所有节点
# 特性:自动创建目录、增量同步、权限保持、进度显示
#===============================================================================
set -euo pipefail

# 配置区
readonly HOSTS=("hadoop102" "hadoop103" "hadoop104")
readonly USER=$(whoami)

# 颜色
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly RED='\033[0;31m'
readonly NC='\033[0m'

log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }

usage() {
    cat << EOF
Usage: $0 <file_or_directory> [file_or_directory...]

将文件或目录同步到集群所有节点: ${HOSTS[*]}

Examples:
    $0 /opt/module/hadoop-3.1.3/etc/hadoop/core-site.xml
    $0 /home/$USER/.bashrc
    $0 /opt/module/hadoop-3.1.3/etc/hadoop/
EOF
    exit 1
}

# 同步到单台服务器
sync_to_host() {
    local host="$1"
    local file="$2"
    local pdir="$3"
    local fname="$4"
    
    # 检查远程目录是否存在,不存在则创建
    ssh -o ConnectTimeout=5 "$host" "mkdir -p $pdir" 2>/dev/null || {
        log_error "无法连接到 $host"
        return 1
    }
    
    # 使用rsync进行增量同步
    if rsync -avz --progress "$file" "${USER}@${host}:${pdir}/${fname}" 2>/dev/null; then
        log_info "[$host] 同步成功: $fname"
        return 0
    else
        log_error "[$host] 同步失败: $fname"
        return 1
    fi
}

# 主程序
main() {
    [[ $# -lt 1 ]] && usage
    
    local total=0
    local success=0
    local failed=0
    
    for file in "$@"; do
        # 检查文件是否存在
        if [[ ! -e "$file" ]]; then
            log_error "文件不存在: $file"
            ((failed++))
            continue
        fi
        
        # 获取父目录和文件名
        local pdir=$(cd -P "$(dirname "$file")" && pwd)
        local fname=$(basename "$file")
        
        log_info "准备同步: $file"
        log_info "目标路径: $pdir/$fname"
        
        # 同步到每台服务器
        for host in "${HOSTS[@]}"; do
            ((total++))
            if sync_to_host "$host" "$file" "$pdir" "$fname"; then
                ((success++))
            else
                ((failed++))
            fi
        done
    done
    
    echo ""
    log_info "========== 同步完成 =========="
    log_info "总计: $total, 成功: $success, 失败: $failed"
}

main "$@"

脚本3:服务器健康巡检+钉钉告警脚本

bash 复制代码
#!/bin/bash
#===============================================================================
# 服务器健康巡检脚本
# 功能:检查CPU、内存、磁盘、负载,异常时发送钉钉告警
# 建议:加入crontab,每5分钟执行一次
#===============================================================================
set -euo pipefail

# 配置区
readonly THRESHOLD_CPU=80
readonly THRESHOLD_MEM=80
readonly THRESHOLD_DISK=85
readonly THRESHOLD_LOAD=5.0
readonly ALERT_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
readonly LOG_FILE="/var/log/server-check.log"

# 确保日志目录存在
mkdir -p "$(dirname "$LOG_FILE")"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# 发送钉钉告警
send_alert() {
    local title="$1"
    local content="$2"
    
    # 如果没有配置webhook,只记录日志
    if [[ "$ALERT_WEBHOOK" == *"YOUR_TOKEN"* ]]; then
        log "[ALERT-SKIPPED] 未配置钉钉token: $title"
        return
    fi
    
    local json="{
        \"msgtype\": \"markdown\",
        \"markdown\": {
            \"title\": \"$title\",
            \"text\": \"## 🚨 $title\\n\\n$content\\n\\n> 📍 服务器: $(hostname)\\n> ⏰ 时间: $(date '+%Y-%m-%d %H:%M:%S')\\n> 💻 IP: $(hostname -I | awk '{print $1}')\"
        }
    }"
    
    curl -s -X POST "$ALERT_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d "$json" > /dev/null 2>&1 && log "[ALERT-SENT] $title" || log "[ALERT-FAILED] $title"
}

# 检查CPU
check_cpu() {
    local cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' || echo "0")
    local cpu_usage=$(echo "100 - ${cpu_idle:-0}" | bc)
    cpu_usage=${cpu_usage%.*}
    
    log "CPU使用率: ${cpu_usage}%"
    
    if [[ ${cpu_usage:-0} -gt $THRESHOLD_CPU ]]; then
        send_alert "CPU告警 🔥" \
            "**当前CPU使用率: ${cpu_usage}%**\n**阈值: ${THRESHOLD_CPU}%**\n\n建议: 检查是否有异常进程"
    fi
}

# 检查内存
check_memory() {
    local mem_info=$(free | grep Mem || echo "0 0 0")
    local total=$(echo "$mem_info" | awk '{print $2}')
    local used=$(echo "$mem_info" | awk '{print $3}')
    
    [[ -z "$total" || "$total" == "0" ]] && return
    
    local usage=$(echo "scale=2; $used / $total * 100" | bc)
    usage=${usage%.*}
    
    log "内存使用率: ${usage}%"
    
    if [[ ${usage:-0} -gt $THRESHOLD_MEM ]]; then
        send_alert "内存告警 💾" \
            "**当前内存使用率: ${usage}%**\n**阈值: ${THRESHOLD_MEM}%**\n\n建议: 检查内存泄漏或扩容"
    fi
}

# 检查磁盘
check_disk() {
    while read -r filesystem size used avail usage mount; do
        [[ "$filesystem" == "Filesystem" ]] && continue
        [[ "$filesystem" == "tmpfs" ]] && continue  # 跳过虚拟文件系统
        
        local usage_num=${usage%\%}
        
        log "磁盘 [$mount] 使用率: $usage"
        
        if [[ ${usage_num:-0} -gt $THRESHOLD_DISK ]]; then
            send_alert "磁盘告警 💿" \
                "**挂载点: $mount**\n**使用率: $usage**\n**阈值: ${THRESHOLD_DISK}%**\n\n建议: 清理日志或扩容"
        fi
    done < <(df -h | grep -v "^Filesystem")
}

# 检查负载
check_load() {
    local load1=$(uptime | awk -F'load average:' '{print $2}' | awk '{print $1}' | tr -d ',')
    
    log "1分钟负载: $load1"
    
    if (( $(echo "$load1 > $THRESHOLD_LOAD" | bc -l) )); then
        send_alert "负载告警 ⚡" \
            "**当前1分钟负载: $load1**\n**阈值: ${THRESHOLD_LOAD}**\n\n建议: 检查是否有CPU密集型任务"
    fi
}

# 主程序
main() {
    log "========== 巡检开始 =========="
    check_cpu
    check_memory
    check_disk
    check_load
    log "========== 巡检完成 =========="
}

main

五、调试技巧:脚本出错了怎么办?

5.1 五级调试法

级别 方法 适用场景
L1:语法检查 bash -n script.sh 执行前检查语法错误
L2:追踪执行 bash -x script.sh 查看每条命令的执行结果
L3:局部调试 set -x / set +x 只调试脚本中的某一段
L4:变量打印 echo "DEBUG: var=$var" 快速查看变量值
L5:错误定位 trap '...' ERR 自动捕获错误位置

5.2 实战调试示例

bash 复制代码
#!/bin/bash

# 方法1:整个脚本追踪
# bash -x debug-demo.sh

# 方法2:局部追踪
set -x  # 开启追踪

# 需要调试的代码段
result=$(ls /nonexistent 2>/dev/null)
echo "result=$result"

set +x  # 关闭追踪

# 方法3:使用trap捕获错误
trap 'echo "[ERROR] 脚本 $0 在第 $LINENO 行失败,退出码: $?"' ERR

# 模拟错误
false  # 这条命令会触发trap

# 方法4:调试函数(带调用栈)
debug() {
    local msg="$1"
    local line=${BASH_LINENO[0]}
    local func=${FUNCNAME[1]:-main}
    echo "[DEBUG] [$func:$line] $msg" >&2
}

my_function() {
    local var="test"
    debug "var=$var"
}

my_function

六、高频面试题(建议收藏)

题目 答案要点
set -euo pipefail 各参数含义? -e出错即退,-u未定义变量报错,-o pipefail管道失败传递
[ ][[ ]] 的区别? [[ ]]是Bash扩展,支持模式匹配、正则、逻辑运算符,不需要引号
$*$@ 的区别? 双引号下:"$*"合并为一个字符串,"$@"保持为独立参数
如何防止重复执行? 使用锁文件(flock或自定义PID文件)
如何并行执行多个任务? 后台进程 & + wait 等待完成
如何安全地处理文件名含空格? 变量始终用双引号包裹,IFS=处理read
如何获取脚本所在目录? $(cd "$(dirname "$0")" && pwd)

如果觉得本文对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬!你的支持是我持续更新的动力!

相关推荐
stanleyrain2 小时前
Windows 实现 Linux 风格“选中即复制,中键即粘贴”操作指南
linux·运维·windows
Elihuss2 小时前
关于RK3506 的MCU软复位后跑不起问题
linux·单片机·嵌入式硬件
小王C语言2 小时前
Linux给指定用户添加sudo权限
linux·运维·服务器
誰能久伴不乏2 小时前
从底层看透音视频架构:FFmpeg 实时视频推流深度解析
linux·c++·tcp/ip·ffmpeg
浪客灿心2 小时前
Linux数据链路层
linux·网络
落羽的落羽2 小时前
【算法札记】练习 | Week3
linux·服务器·数据结构·c++·人工智能·算法·动态规划
keyipatience2 小时前
Linux进程调度与优先级机制解析
linux·运维·服务器
IT大白鼠2 小时前
Linux系统中应用程序安装及管理
linux·服务器
叶非花3 小时前
Ubuntu服务器性能检测工具NetData安装
linux·服务器·ubuntu