Shell 脚本入门 — 运维自动化第一步

📖 知识点简介

前面学了很多单条命令:grepawkfindsystemctllvextend......但运维的工作不是敲一条命令就收工的。日常巡检、批量操作、定时任务、故障自愈------写脚本才是把这些命令串联成生产力的关键。

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"

⚠️ 常见坑点与注意事项

  1. 变量赋值 = 两边不能加空格name = "value" 会被解释成执行 name 命令并传了两个参数。这是 Shell 新手最常见的错误

  2. 忘加 $ 引用变量if [ count -gt 10 ]count 只是字符串,必须写 $count。变量展开要用 $

  3. 条件判断的中括号两侧要空格[ "$a" = "$b" ] ✅,["$a"="$b"] ❌(语法错误)。

  4. 变量双引号防分词 + glob 展开

    bash 复制代码
    # 文件路径含空格时不加引号会裂开
    file="/data/logs/app $(date).log"
    cat $file         # ❌ 路径裂成多个参数
    cat "$file"       # ✅ 正确
  5. set -e 的意外退出陷阱 :管道中最后一命令失败、grep 没匹配到(退出码 1)都会导致脚本退出。可以加 || true 优雅处理:

    bash 复制代码
    grep "ERROR" app.log || true   # 没找到也不退出
  6. vs $() :老式反引号嵌套困难,统一用 $()

    bash 复制代码
    date=$(date +%F)                   # ✅
    path=$(dirname "$(which nginx)")   # 嵌套无压力
  7. read 不加 -r 会吃掉反斜杠 :处理文件路径、日志行时务必加 -r

    bash 复制代码
    while read -r line; do ... done < file
  8. cron 中执行脚本的环境问题:cron 的环境变量和 PATH 与交互 shell 不同,建议脚本开头固定 PATH:

    bash 复制代码
    #!/bin/bash
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

📌 一句话总结:脚本 = 把之前学的命令用 if/for/while 串起来 + 加判断防出错 + 加日志可追踪。先写小脚本,再拼大工具,运维自动化就这么一步步来。

相关推荐
ping某6 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy6 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom6 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户14748530797411 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody12311 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端
onething36511 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈
一个做软件开发的牛马11 小时前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
码事漫谈12 小时前
AI 编程的「三体」架构:OpenSpec + Superpowers + GStack 如何让一个开发者撑起整个研发团队
后端
吃饱了得干活12 小时前
深入解析 OpenFeign:从重试、拦截到负载均衡的全维度实践
后端
onething36512 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 6 —— 业务完善 + 会话消息预览
人工智能·后端·全栈