Bash 脚本编程说明:从零搭建自动化运维工具箱
说明: 本文为配置思路与示例整理,不代表作者已在自己的服务器上逐项验证全部命令。执行涉及公网暴露、账户权限、数据删除或服务重启的操作前,请先备份,并结合官方文档与实际环境核验。
运维不写脚本,等于用锄头挖矿。本文带你从 Bash 基础语法出发,一步步构建实用的自动化运维脚本,涵盖变量、条件判断、循环、函数、错误处理、日志记录等核心技能,最终实现一个完整的服务器巡检脚本。
为什么运维必须学 Bash?
在 Linux 服务器的世界里,Bash 脚本就像瑞士军刀——它可能不是最优雅的工具,但几乎任何任务都能用它完成。无论是定时备份数据库、批量管理服务器、监控磁盘空间,还是自动化部署流程,Bash 脚本都是最直接、最轻量的选择。
你可能会问:"Python 不是更好吗?"没错,Python 在复杂项目中更强大。但 Bash 的优势在于:
- 零依赖:Linux 系统自带 Bash,不需要安装任何额外的包
- 启动快:不像 Python 需要解释器初始化,Bash 脚本可以秒级执行
- 系统集成深:直接调用系统命令,与 Linux 环境无缝衔接
- 学习曲线低:基础语法几十分钟就能上手
第一章:Bash 基础语法速成
1.1 变量与字符串操作
Bash 中的变量不需要声明类型,直接赋值即可(等号两边不能有空格):
# 变量赋值
name="宏宏"
server_count=3
today=$(date +%Y-%m-%d)
# 使用变量(推荐加花括号,避免歧义)
echo "博客作者:${name}"
echo "服务器数量:${server_count}"
echo "今天日期:${today}"字符串操作是 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 的增强版测试命令,比 [ ] 更安全:
# 文件测试
[[ -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 中常用的循环有三种:for、while 和 until。
# 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 函数封装
函数让脚本模块化、可复用:
# 函数定义
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 参数处理与选项解析
专业的脚本需要接受命令行参数:
#!/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 错误处理与安全模式
较完整的脚本必须有健壮的错误处理:
#!/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 日志系统
专业的脚本需要完善的日志记录:
# 日志配置
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+:
# 索引数组
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、内存、磁盘、网络等关键指标,并生成报告。
#!/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 脚本都应该以这三行开头:
#!/bin/bash
set -euo pipefailset -e:任何命令失败立即退出,避免错误雪崩set -u:使用未定义变量时报错,避免空变量引发的隐蔽 bugset -o pipefail:管道中任何命令失败则整个管道失败
4.2 变量引用的铁律
永远用双引号包裹变量,这是 Bash 中最重要的习惯:
# ❌ 错误示范
rm -rf $DIR/file # 如果 DIR 为空,会执行 rm -rf /file
echo $PATH # 如果 PATH 包含空格,会拆分
# ✅ 正确做法
rm -rf "${DIR}/file"
echo "${PATH}"4.3 临时文件处理
# 创建安全的临时文件
TMPFILE=$(mktemp /tmp/myscript.XXXXXX)
trap 'rm -f "$TMPFILE"' EXIT
# 或者创建临时目录
TMPDIR=$(mktemp -d /tmp/myscript.XXXXXX)
trap 'rm -rf "$TMPDIR"' EXIT4.4 并发执行
利用 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"
done4.5 ShellCheck — 你的脚本质检员
ShellCheck 是一个静态分析工具,能帮你发现脚本中的常见错误:
# 安装 ShellCheck
apt install shellcheck # Debian/Ubuntu
yum install ShellCheck # CentOS
# 检查脚本
shellcheck myscript.sh第五章:常用片段速查
5.1 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"5.2 等待端口就绪
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 305.3 彩色输出
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 锁机制(防止脚本重复执行)
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% 的运维脚本需求。
评论
游客无需注册即可评论。
你提交的昵称、邮箱、网址和评论内容会保存在服务端,用于展示评论身份、接收回复及必要的安全审计。
浏览器会本地保存已填游客信息和评论草稿,方便下次免填。
回复提醒会通过站内消息和邮件通知。