📖 知识点简介
前面学了很多单条命令:grep、awk、find、systemctl、lvextend......但运维的工作不是敲一条命令就收工的。日常巡检、批量操作、定时任务、故障自愈------写脚本才是把这些命令串联成生产力的关键。
Shell 脚本是 Linux 运维的"胶水语言",不需要编译,也不需要安装额外环境,一个 .sh 文件加个 #!/bin/bash 就能跑。今天从运维视角出发,系统梳理脚本编写的基础范式、控制结构、函数封装与调试技巧。
🛠 核心语法与命令整理
1. 脚本骨架与执行
bash
#!/bin/bash
# 上面这行叫 shebang,告诉系统用 bash 解释执行
# 执行方式
chmod +x script.sh
./script.sh # 需要执行权限
bash script.sh # 不需要执行权限(当前 shell 执行)
source script.sh # 在当前 shell 环境执行(变量会保留)
. script.sh # source 的简写
2. 变量与特殊符号
| 语法 | 说明 | 示例 |
|---|---|---|
name="value" |
变量赋值(= 两边不能有空格) | log_dir="/var/log/app" |
$var / ${var} |
变量引用,花括号防歧义 | echo "日志路径: ${log_dir}" |
$0 |
脚本文件名 | ./backup.sh → $0=./backup.sh |
$1 $2 ... $9 |
位置参数(第1/2...个参数) | ./backup.sh /data → $1=/data |
${10} |
第10个及以后的参数需花括号 | --- |
$# |
参数个数 | --- |
$@ |
所有参数(每个独立) | 遍历参数用 |
$* |
所有参数(作为单个字符串) | --- |
$? |
上一条命令的退出码(0=成功) | 判断是否执行成功 |
$$ |
当前脚本的 PID | 日志中标记进程 |
$(command) |
命令替换,将命令输出赋给变量 | today=$(date +%F) |
3. 条件判断
bash
# if 基本结构
if [ 条件 ]; then
# 条件成立执行
elif [ 条件 ]; then
# 另一个条件
else
# 都不成立
fi
# 数值比较(注意:中括号内两侧必须空格)
[ "$count" -eq 10 ] # 等于
[ "$count" -ne 10 ] # 不等于
[ "$count" -gt 10 ] # 大于
[ "$count" -lt 10 ] # 小于
[ "$count" -ge 10 ] # 大于等于
[ "$count" -le 10 ] # 小于等于
# 字符串比较
[ "$str" = "hello" ] # 等于(单个 =)
[ "$str" != "hello" ] # 不等于
[ -z "$str" ] # 空字符串
[ -n "$str" ] # 非空字符串
# 文件判断(运维最常用!)
[ -f "$file" ] # 文件存在且是普通文件
[ -d "$dir" ] # 目录存在
[ -e "$path" ] # 路径存在
[ -s "$file" ] # 文件存在且非空
[ -r "$file" ] # 可读
[ -w "$file" ] # 可写
[ -x "$file" ] # 可执行
[ "$file1" -nt "$file2" ] # file1 比 file2 新(newer than)
4. 循环结构
bash
# for 循环 --- 遍历列表
for ip in 10.0.0.{1..20}; do
ping -c 1 -W 1 "$ip" &>/dev/null && echo "$ip is alive"
done
# for 循环 --- C 风格
for ((i=0; i<10; i++)); do
echo "count: $i"
done
# while 循环 --- 读文件逐行处理
while read -r line; do
echo "处理: $line"
done < /var/log/app/error.log
# 死循环(后台监控脚本常用)
while true; do
if ! pgrep -x nginx > /dev/null; then
systemctl restart nginx
echo "nginx 重启" | logger -t monitor
fi
sleep 30
done
5. 函数封装
bash
# 定义格式一(推荐)
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}
# 定义格式二
function check_disk() {
local mount_point="$1" # local 声明局部变量
local threshold="${2:-80}" # 默认阈值 80%
df -h "$mount_point" | awk 'NR==2 {print $5}' | sed 's/%//'
}
# 调用
log_info "开始巡检"
usage=$(check_disk "/data" 85)
log_info "/data 使用率: ${usage}%"
6. 常用调试技巧
bash
# 执行时开启调试模式,显示每行命令及其结果
bash -x script.sh
# 脚本头加 set 系列
set -x # 命令执行前打印(类似 -x 参数)
set -e # 任何命令失败立即退出(避免"继续执行错误的脚本")
set -u # 使用未定义变量时报错退出(而不是静默用空值)
set -o pipefail # 管道中任一命令失败即整体失败
# 常用组合(脚本开头加这一行)
set -euo pipefail
# 临时调试一段代码
set -x
# ... 待调试代码 ...
set +x
💻 实操示例
示例 1:服务器健康巡检脚本(一键跑完)
bash
#!/bin/bash
set -euo pipefail
HOSTNAME=$(hostname)
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "══════════════════════════════════════════"
echo " 服务器巡检报告 --- $HOSTNAME"
echo " 时间: $DATE"
echo "══════════════════════════════════════════"
# 1. 系统运行时间与负载
echo ""
echo "▶ 系统负载"
uptime
# 2. CPU 使用率(取 idle 反向算)
cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d, -f1)
cpu_usage=$(echo "100 - $cpu_idle" | bc)
echo "CPU 使用率: ${cpu_usage}%"
# 3. 内存使用
echo ""
echo "▶ 内存状态"
free -h | awk 'NR==2{printf "总内存: %s | 已用: %s | 可用: %s\n", $2, $3, $7}'
# 4. 磁盘使用率超过 80% 的挂载点高亮
echo ""
echo "▶ 磁盘空间"
df -h | awk 'NR>1{
gsub(/%/,"",$5);
if ($5 > 80) printf "⚠️ 告警: %s 使用率 %d%%\n", $6, $5;
else if ($5 > 60) printf " 注意: %s 使用率 %d%%\n", $6, $5
}'
# 5. 监听端口
echo ""
echo "▶ 关键服务端口"
for port in 80 443 3306 6379; do
if ss -tunlp | grep -q ":$port "; then
echo " 端口 $port ✓ 已监听"
else
echo " 端口 $port ✗ 未监听"
fi
done
# 6. 最近 10 条系统错误日志
echo ""
echo "▶ 最近系统错误日志"
journalctl -p err -n 10 --no-pager 2>/dev/null || echo " 无错误日志"
echo ""
echo "══════════════════════════════════════════"
echo " 巡检完成 ✅"
echo "══════════════════════════════════════════"
示例 2:半小时内检测 SSH 爆破并封禁 IP
bash
#!/bin/bash
# block_ssh_brute.sh --- 检测并封禁 SSH 暴力破解 IP
set -euo pipefail
LOG_FILE="/var/log/secure"
THRESHOLD=5
BLOCK_LIST="/tmp/blocked_ips.txt"
touch "$BLOCK_LIST"
# 从日志中提取最近 30 分钟失败超过 threshold 次的 IP
grep "Failed password" "$LOG_FILE" | \
awk '{print $(NF-3)}' | \
sort | uniq -c | sort -rn | \
while read -r count ip; do
if [ "$count" -ge "$THRESHOLD" ] && ! grep -q "^$ip$" "$BLOCK_LIST"; then
echo "🚫 封禁 $ip(失败次数: $count)"
iptables -A INPUT -s "$ip" -j DROP
echo "$ip" >> "$BLOCK_LIST"
logger -t ssh-block "Blocked SSH brute force IP: $ip"
fi
done
示例 3:批量部署 SSH 公钥
bash
#!/bin/bash
# deploy_ssh_key.sh --- 向一批服务器推送公钥
set -euo pipefail
KEY_FILE="${HOME}/.ssh/id_rsa.pub"
SERVER_LIST="servers.txt"
USER="deploy"
PORT=22
if [ ! -f "$KEY_FILE" ]; then
echo "❌ 公钥文件 $KEY_FILE 不存在,请先执行 ssh-keygen"
exit 1
fi
if [ ! -f "$SERVER_LIST" ]; then
echo "❌ 服务器列表 $SERVER_LIST 不存在"
exit 1
fi
echo "开始推送公钥,共 $(wc -l < "$SERVER_LIST") 台服务器"
while read -r server; do
[ -z "$server" ] && continue
echo -n "→ $server ... "
ssh-copy-id -i "$KEY_FILE" -p "$PORT" "${USER}@${server}" 2>/dev/null && \
echo "✅" || echo "❌ 失败"
done < "$SERVER_LIST"
⚠️ 常见坑点与注意事项
-
变量赋值 = 两边不能加空格 :
name = "value"会被解释成执行name命令并传了两个参数。这是 Shell 新手最常见的错误。 -
忘加
$引用变量 :if [ count -gt 10 ]中count只是字符串,必须写$count。变量展开要用$。 -
条件判断的中括号两侧要空格 :
[ "$a" = "$b" ]✅,["$a"="$b"]❌(语法错误)。 -
变量双引号防分词 + glob 展开:
bash# 文件路径含空格时不加引号会裂开 file="/data/logs/app $(date).log" cat $file # ❌ 路径裂成多个参数 cat "$file" # ✅ 正确 -
set -e的意外退出陷阱 :管道中最后一命令失败、grep 没匹配到(退出码 1)都会导致脚本退出。可以加|| true优雅处理:bashgrep "ERROR" app.log || true # 没找到也不退出 -
vs$():老式反引号嵌套困难,统一用$():bashdate=$(date +%F) # ✅ path=$(dirname "$(which nginx)") # 嵌套无压力 -
read不加-r会吃掉反斜杠 :处理文件路径、日志行时务必加-r:bashwhile read -r line; do ... done < file -
cron 中执行脚本的环境问题:cron 的环境变量和 PATH 与交互 shell 不同,建议脚本开头固定 PATH:
bash#!/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
📌 一句话总结:脚本 = 把之前学的命令用
if/for/while串起来 + 加判断防出错 + 加日志可追踪。先写小脚本,再拼大工具,运维自动化就这么一步步来。