AI 摘要
AI
正在生成摘要...

Bash 脚本编程说明:从零搭建自动化运维工具箱

说明: 本文为配置思路与示例整理,不代表作者已在自己的服务器上逐项验证全部命令。执行涉及公网暴露、账户权限、数据删除或服务重启的操作前,请先备份,并结合官方文档与实际环境核验。

运维不写脚本,等于用锄头挖矿。本文带你从 Bash 基础语法出发,一步步构建实用的自动化运维脚本,涵盖变量、条件判断、循环、函数、错误处理、日志记录等核心技能,最终实现一个完整的服务器巡检脚本。

为什么运维必须学 Bash?

在 Linux 服务器的世界里,Bash 脚本就像瑞士军刀——它可能不是最优雅的工具,但几乎任何任务都能用它完成。无论是定时备份数据库、批量管理服务器、监控磁盘空间,还是自动化部署流程,Bash 脚本都是最直接、最轻量的选择。

你可能会问:"Python 不是更好吗?"没错,Python 在复杂项目中更强大。但 Bash 的优势在于:

  • 零依赖:Linux 系统自带 Bash,不需要安装任何额外的包
  • 启动快:不像 Python 需要解释器初始化,Bash 脚本可以秒级执行
  • 系统集成深:直接调用系统命令,与 Linux 环境无缝衔接
  • 学习曲线低:基础语法几十分钟就能上手

第一章:Bash 基础语法速成

1.1 变量与字符串操作

Bash 中的变量不需要声明类型,直接赋值即可(等号两边不能有空格):

BASH
# 变量赋值
name="宏宏"
server_count=3
today=$(date +%Y-%m-%d)

# 使用变量(推荐加花括号,避免歧义)
echo "博客作者:${name}"
echo "服务器数量:${server_count}"
echo "今天日期:${today}"

字符串操作是 Bash 脚本中最常用的技能:

BASH
filename="/var/log/nginx/access.log"

# 获取文件名(去除路径和扩展名)
basename="${filename##*/}"          # access.log
dirname="${filename%/*}"           # /var/log/nginx
extension="${filename##*.}"        # log
name_no_ext="${basename%.*}"       # access

# 字符串替换
text="Hello, World!"
echo "${text/World/Bash}"          # Hello, Bash!

# 字符串长度
echo "${#text}"                    # 14

# 默认值
echo "${undefined_var:-默认值}"     # 默认值
echo "${undefined_var:=默认值}"     # 默认值(同时赋值)

1.2 条件判断

条件判断是脚本逻辑的核心。[[ ]] 是 Bash 的增强版测试命令,比 [ ] 更安全:

BASH
# 文件测试
[[ -f "/etc/passwd" ]] && echo "文件存在"
[[ -d "/var/log" ]] && echo "目录存在"
[[ -r "$file" ]] && echo "文件可读"
[[ -w "$file" ]] && echo "文件可写"
[[ -s "$file" ]] && echo "文件非空"

# 字符串比较
[[ "$str1" == "$str2" ]] && echo "相等"
[[ "$str1" != "$str2" ]] && echo "不相等"
[[ -z "$str" ]] && echo "字符串为空"
[[ -n "$str" ]] && echo "字符串非空"

# 数值比较(用 -eq -ne -lt -le -gt -ge)
[[ $count -gt 10 ]] && echo "数量大于10"

# 逻辑组合(&& 和 || 可以替代 if)
[[ -f "$config" ]] && source "$config" || echo "配置文件不存在"

1.3 循环结构

Bash 中常用的循环有三种:forwhileuntil

BASH
# for 循环 - 遍历列表
for server in web01 web02 web03; do
    echo "正在检查:${server}"
    ssh "$server" "uptime"
done

# for 循环 - C 风格(适合数值循环)
for ((i=1; i<=10; i++)); do
    echo "第 ${i} 次循环"
done

# for 循环 - 范围遍历
for port in {80,443,8080,8443}; do
    nc -zv localhost "$port" 2>/dev/null && echo "端口 ${port} 开放"
done

# while 循环 - 逐行读取文件
while IFS= read -r line; do
    echo "处理行:${line}"
done < /etc/hosts

# while 循环 - 等待条件满足
while ! nc -zv localhost 5432 2>/dev/null; do
    echo "等待 PostgreSQL 启动..."
    sleep 2
done
echo "PostgreSQL 已就绪"

1.4 函数封装

函数让脚本模块化、可复用:

BASH
# 函数定义
log_info() {
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[INFO] ${timestamp} - $*"
}

log_error() {
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[ERROR] ${timestamp} - $*" >&2
}

# 函数返回值(用 echo 输出,用 $() 捕获)
get_disk_usage() {
    local path="${1:-/}"
    df -h "$path" | awk 'NR==2{print $5}' | tr -d '%'
}

# 使用函数
usage=$(get_disk_usage "/var")
if [[ $usage -gt 80 ]]; then
    log_error "磁盘使用率过高:${usage}%"
fi

第二章:进阶技巧

2.1 参数处理与选项解析

专业的脚本需要接受命令行参数:

BASH
#!/bin/bash
set -euo pipefail

# 简单参数处理
VERBOSE=false
DRY_RUN=false
TARGET_DIR="/var/log"

while [[ $# -gt 0 ]]; do
    case "$1" in
        -v|--verbose)
            VERBOSE=true
            shift
            ;;
        -n|--dry-run)
            DRY_RUN=true
            shift
            ;;
        -d|--dir)
            TARGET_DIR="$2"
            shift 2
            ;;
        -h|--help)
            echo "用法: $0 [-v] [-n] [-d DIR]"
            echo "  -v, --verbose    详细输出"
            echo "  -n, --dry-run    仅模拟执行"
            echo "  -d, --dir DIR    指定目录"
            exit 0
            ;;
        *)
            echo "未知参数:$1" >&2
            exit 1
            ;;
    esac
done

$VERBOSE && echo "目标目录:${TARGET_DIR}"

2.2 错误处理与安全模式

较完整的脚本必须有健壮的错误处理:

BASH
#!/bin/bash

# 安全模式 - 这一行必须放在脚本开头
set -euo pipefail
# -e:任何命令失败立即退出
# -u:使用未定义变量报错
# -o pipefail:管道中任何命令失败则整体失败

# 错误捕获陷阱
trap 'echo "脚本在第 $LINENO 行出错,退出码:$?" >&2' ERR

# 清理函数(脚本退出时自动执行)
cleanup() {
    local exit_code=$?
    # 清理临时文件
    [[ -d "${TMP_DIR:-}" ]] && rm -rf "$TMP_DIR"
    # 恢复原始配置
    [[ -f "${BACKUP_FILE:-}" ]] && cp "$BACKUP_FILE" "$ORIGINAL_CONFIG"
    exit "$exit_code"
}
trap cleanup EXIT

# 创建临时目录
TMP_DIR=$(mktemp -d)
echo "临时目录:${TMP_DIR}"

2.3 日志系统

专业的脚本需要完善的日志记录:

BASH
# 日志配置
LOG_DIR="/var/log/myscripts"
LOG_FILE="${LOG_DIR}/$(basename "$0" .sh).log"

# 确保日志目录存在
mkdir -p "$LOG_DIR"

# 日志函数
log() {
    local level="$1"
    shift
    local msg="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local log_line="[${timestamp}] [${level}] ${msg}"

    # 同时输出到终端和文件
    echo "$log_line" | tee -a "$LOG_FILE"
}

log_info()  { log "INFO"  "$@"; }
log_warn()  { log "WARN"  "$@"; }
log_error() { log "ERROR" "$@"; }

# 带颜色的终端输出
log_success() {
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo -e "[${timestamp}] [\033[32mOK\033[0m] $*" | tee -a "$LOG_FILE"
}

2.4 数组与关联数组

Bash 支持数组,关联数组(类似字典)需要 Bash 4.0+:

BASH
# 索引数组
servers=("web01" "web02" "db01" "cache01")
echo "服务器数量:${#servers[@]}"
echo "第一个服务器:${servers[0]}"

# 遍历数组
for server in "${servers[@]}"; do
    echo "检查 ${server}..."
done

# 关联数组(需要 Bash 4.0+)
declare -A server_info
server_info[web01]="192.168.1.10:Nginx"
server_info[web02]="192.168.1.11:Nginx"
server_info[db01]="192.168.1.20:PostgreSQL"

# 遍历关联数组
for server in "${!server_info[@]}"; do
    IFS=':' read -r ip service <<< "${server_info[$server]}"
    echo "${server} -> IP: ${ip}, 服务: ${service}"
done

第三章:实战项目 — 服务器巡检脚本

理论讲完了,来看一个完整的实战案例。这个脚本可以巡检服务器的 CPU、内存、磁盘、网络等关键指标,并生成报告。

BASH
#!/bin/bash
set -euo pipefail

#=========================================
# 服务器巡检脚本 v1.0
# 功能:检查服务器关键指标,生成巡检报告
#=========================================

# --- 配置 ---
REPORT_DIR="/var/log/server-check"
DATE=$(date '+%Y-%m-%d_%H-%M-%S')
REPORT_FILE="${REPORT_DIR}/report_${DATE}.html"
DISK_WARN=80
DISK_CRIT=90
MEM_WARN=80
CPU_WARN=80

# --- 初始化 ---
mkdir -p "$REPORT_DIR"

# --- 工具函数 ---
log() {
    echo "[$(date '+%H:%M:%S')] $*"
}

get_color() {
    local value=$1
    local warn=$2
    local crit=$3
    if [[ $value -ge $crit ]]; then
        echo "#ff4444"
    elif [[ $value -ge $warn ]]; then
        echo "#ffaa00"
    else
        echo "#44bb44"
    fi
}

# --- 巡检模块 ---

check_cpu() {
    log "检查 CPU 使用率..."
    # 取 1 秒内的平均负载
    local cpu_usage
    cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print int($2 + $4)}')
    local color
    color=$(get_color "$cpu_usage" "$CPU_WARN" 95)
    echo "<tr><td>CPU 使用率</td><td style='color:${color}'>${cpu_usage}%</td></tr>"
}

check_memory() {
    log "检查内存使用率..."
    local mem_info
    mem_info=$(free -m | awk 'NR==2{
        printf "%d %d %d", $3*100/$2, $3, $2
    }')
    read -r mem_pct mem_used mem_total <<< "$mem_info"
    local color
    color=$(get_color "$mem_pct" "$MEM_WARN" 95)
    echo "<tr><td>内存使用率</td><td style='color:${color}'>${mem_pct}% (${mem_used}MB / ${mem_total}MB)</td></tr>"
}

check_disk() {
    log "检查磁盘使用率..."
    local output=""
    while IFS= read -r line; do
        local mount pct
        mount=$(echo "$line" | awk '{print $6}')
        pct=$(echo "$line" | awk '{print int($5)}')
        local color
        color=$(get_color "$pct" "$DISK_WARN" "$DISK_CRIT")
        output+="<tr><td>磁盘 ${mount}</td><td style='color:${color}'>${pct}%</td></tr>"
    done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs | tail -n +2)
    echo "$output"
}

check_network() {
    log "检查网络连通性..."
    local targets=("8.8.8.8" "1.1.1.1" "223.5.5.5")
    local output=""
    for target in "${targets[@]}"; do
        local latency
        latency=$(ping -c1 -W3 "$target" 2>/dev/null | grep -oP 'time=\K[\d.]+' || echo "超时")
        local color="#44bb44"
        [[ "$latency" == "超时" ]] && color="#ff4444"
        output+="<tr><td>Ping ${target}</td><td style='color:${color}'>${latency}ms</td></tr>"
    done
    echo "$output"
}

check_listening_ports() {
    log "检查监听端口..."
    local ports
    ports=$(ss -tlnp 2>/dev/null | tail -n +2 | awk '{print $4, $6}' | head -10)
    local output=""
    while IFS= read -r line; do
        output+="<tr><td>端口</td><td>${line}</td></tr>"
    done <<< "$ports"
    echo "$output"
}

check_system_info() {
    log "收集系统信息..."
    local hostname kernel uptime_info
    hostname=$(hostname -f 2>/dev/null || hostname)
    kernel=$(uname -r)
    uptime_info=$(uptime -p 2>/dev/null || uptime)
    echo "<tr><td>主机名</td><td>${hostname}</td></tr>"
    echo "<tr><td>内核版本</td><td>${kernel}</td></tr>"
    echo "<tr><td>运行时间</td><td>${uptime_info}</td></tr>"
}

# --- 生成 HTML 报告 ---
generate_report() {
    cat > "$REPORT_FILE" << 'REPORT_END'
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>服务器巡检报告</title>
    <style>
        body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; background: #f5f5f5; }
        h1 { color: #333; border-bottom: 2px solid #4a90d9; padding-bottom: 10px; }
        table { width: 100%; border-collapse: collapse; background: white; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; }
        th { background: #4a90d9; color: white; }
        tr:hover { background: #f0f8ff; }
        .section-title { background: #e8f4fd; font-weight: bold; padding: 10px 16px; }
        .footer { color: #999; font-size: 12px; margin-top: 30px; text-align: center; }
    </style>
</head>
<body>
<h1>🖥️ 服务器巡检报告</h1>
<table>
REPORT_END

    # 填充内容
    {
        echo "<tr><td class='section-title' colspan='2'>📋 系统信息</td></tr>"
        check_system_info
        echo "<tr><td class='section-title' colspan='2'>⚡ 性能指标</td></tr>"
        check_cpu
        check_memory
        check_disk
        echo "<tr><td class='section-title' colspan='2'>🌐 网络状态</td></tr>"
        check_network
        check_listening_ports
    } >> "$REPORT_FILE"

    cat >> "$REPORT_FILE" << 'REPORT_END2'
</table>
<div class="footer">由服务器巡检脚本自动生成</div>
</body>
</html>
REPORT_END2
}

# --- 主流程 ---
main() {
    log "========== 开始服务器巡检 =========="

    {
        echo "<!-- 巡检时间:$(date) -->"
    } >> /dev/null

    generate_report

    log "巡检完成,报告已生成:${REPORT_FILE}"
    log "========== 巡检结束 =========="
}

main "$@"

第四章:建议与避坑指南

4.1 脚本开头的黄金三行

每个较完整的 Bash 脚本都应该以这三行开头:

BASH
#!/bin/bash
set -euo pipefail
  • set -e:任何命令失败立即退出,避免错误雪崩
  • set -u:使用未定义变量时报错,避免空变量引发的隐蔽 bug
  • set -o pipefail:管道中任何命令失败则整个管道失败

4.2 变量引用的铁律

永远用双引号包裹变量,这是 Bash 中最重要的习惯:

BASH
# ❌ 错误示范
rm -rf $DIR/file        # 如果 DIR 为空,会执行 rm -rf /file
echo $PATH               # 如果 PATH 包含空格,会拆分

# ✅ 正确做法
rm -rf "${DIR}/file"
echo "${PATH}"

4.3 临时文件处理

BASH
# 创建安全的临时文件
TMPFILE=$(mktemp /tmp/myscript.XXXXXX)
trap 'rm -f "$TMPFILE"' EXIT

# 或者创建临时目录
TMPDIR=$(mktemp -d /tmp/myscript.XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT

4.4 并发执行

利用 Bash 的后台进程实现并发:

BASH
# 并发检查多台服务器
pids=()
for server in "${servers[@]}"; do
    (
        if ssh -o ConnectTimeout=5 "$server" "true" 2>/dev/null; then
            echo "[OK] ${server}"
        else
            echo "[FAIL] ${server}"
        fi
    ) &
    pids+=($!)
done

# 等待所有后台任务完成
for pid in "${pids[@]}"; do
    wait "$pid"
done

4.5 ShellCheck — 你的脚本质检员

ShellCheck 是一个静态分析工具,能帮你发现脚本中的常见错误:

BASH
# 安装 ShellCheck
apt install shellcheck    # Debian/Ubuntu
yum install ShellCheck    # CentOS

# 检查脚本
shellcheck myscript.sh

第五章:常用片段速查

5.1 获取脚本所在目录

BASH
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

5.2 等待端口就绪

BASH
wait_for_port() {
    local host="$1" port="$2" timeout="${3:-30}"
    local start=$(date +%s)
    while ! nc -z "$host" "$port" 2>/dev/null; do
        if (( $(date +%s) - start > timeout )); then
            echo "等待端口 ${port} 超时(${timeout}s)" >&2
            return 1
        fi
        sleep 1
    done
    echo "端口 ${port} 已就绪"
}

# 用法
wait_for_port localhost 5432 30

5.3 彩色输出

BASH
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

echo -e "${GREEN}成功${NC}:操作已完成"
echo -e "${YELLOW}警告${NC}:磁盘空间不足"
echo -e "${RED}错误${NC}:连接失败"

5.4 锁机制(防止脚本重复执行)

BASH
LOCK_FILE="/var/lock/myscript.lock"

# 尝试获取锁
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
    echo "另一个实例正在运行,退出" >&2
    exit 1
fi

# 脚本正常执行...
# 锁会在脚本退出时自动释放

总结

Bash 脚本不是最强大的编程语言,但它是运维工程师最趁手的工具。掌握本文介绍的技能,你就能:

  • ✅ 编写结构清晰、易于维护的脚本
  • ✅ 处理复杂的条件逻辑和循环
  • ✅ 实现完善的错误处理和日志记录
  • ✅ 自动化日常运维任务
  • ✅ 构建可复用的运维工具箱

记住:好的脚本不是一次写成的,而是不断迭代优化出来的。 先让脚本跑起来,再逐步加入错误处理、日志、配置文件等高级特性。

💡 下一步学习建议:当你发现 Bash 在某些场景下力不从心时(比如复杂的 JSON 处理、并发任务管理),可以考虑学习 Python 或 Go 来补充你的工具箱。但在此之前,先把 Bash 用好,它能解决 80% 的运维脚本需求。

评论