最后更新于:2026年7月

Shell 脚本自动化运维
Shell:运维人员的瑞士军刀

如果你在 Linux 上重复执行同一个操作超过 3 次,就该写脚本了。 Shell 脚本是运维自动化的基石——不需要安装额外环境,不需要编译,写完就能跑。从简单的日志清理到复杂的服务部署,Shell 脚本都能胜任。

但很多人写 Shell 脚本全靠复制粘贴,遇到引号转义就懵,脚本出错不知道怎么调试,更不知道 set -euo pipefail 这种安全选项。

本文从基础语法到生产级脚本,带你全面掌握 Shell 自动化运维。


一、Shell 脚本基础

1.1 什么是 Shell?

Shell 是用户与 Linux 内核之间的接口,你输入的命令都由 Shell 解释执行。

常见 Shell:

Shell说明路径
Bash最常用,Linux 默认/bin/bash
ZshmacOS 默认,功能更丰富/bin/zsh
Fish用户友好,不支持 POSIX/usr/bin/fish
ShPOSIX Shell,最基础/bin/sh
Dash轻量,Debian 默认 sh/usr/bin/dash

本文以 Bash 为主,因为它是 Linux 最通用的 Shell。

1.2 第一个脚本

BASH
#!/bin/bash
# 这是一个注释
echo "Hello, World!"
echo "当前用户: $(whoami)"
echo "当前目录: $(pwd)"
echo "系统时间: $(date '+%Y-%m-%d %H:%M:%S')"

运行脚本:

BASH
# 方式1:添加执行权限
chmod +x script.sh
./script.sh

# 方式2:直接用 bash 执行
bash script.sh

# 方式3:source 执行(在当前 Shell 中运行)
source script.sh
# 或
. script.sh

Shebang(#!)说明:

BASH
#!/bin/bash           # 用 bash 执行
#!/usr/bin/env bash   # 更通用的写法(推荐)
#!/bin/sh             # 用 sh 执行(POSIX 兼容)
#!/usr/bin/env python3  # 用 python3 执行

1.3 脚本安全选项

生产级脚本必须加的安全头:

BASH
#!/usr/bin/env bash
set -euo pipefail

# 解释:
# set -e  → 遇到错误立即退出
# set -u  → 使用未定义变量时报错
# set -o pipefail  → 管道中任一命令失败则整个失败

更严格的调试模式:

BASH
#!/usr/bin/env bash
set -euo pipefail

# 调试模式(打印每条执行的命令)
# set -x  # 开启调试
# set +x  # 关闭调试

# 或在运行时开启
# bash -x script.sh

二、变量与数据类型

2.1 变量定义

BASH
#!/usr/bin/env bash

# 基本变量(等号两边不能有空格!)
name="Alice"
age=30
PI=3.14159

# 使用变量(推荐加花括号)
echo "姓名: ${name}"
echo "年龄: ${age}"

# 只读变量
readonly VERSION="1.0.0"
# VERSION="2.0.0"  # 报错

# 删除变量
unset temp_var

2.2 字符串操作

BASH
#!/usr/bin/env bash

str="Hello, World!"

# 字符串长度
echo ${#str}  # 13

# 子字符串(从位置0开始,取5个字符)
echo ${str:0:5}  # Hello

# 从位置7开始到末尾
echo ${str:7}  # World!

# 字符串替换(替换第一个)
echo ${str/World/Shell}  # Hello, Shell!

# 字符串替换(替换所有)
str2="a,b,c,d"
echo ${str2//,/-}  # a-b-c-d

# 删除前缀
file="archive.tar.gz"
echo ${file#*.}   # tar.gz(删除最短匹配前缀)
echo ${file##*.}  # gz(删除最长匹配前缀)

# 删除后缀
echo ${file%.*}   # archive.tar(删除最短匹配后缀)
echo ${file%%.*}  # archive(删除最长匹配后缀)

# 大小写转换
echo ${str,,}  # hello, world!(转小写)
echo ${str^^}  # HELLO, WORLD!(转大写)

2.3 特殊变量

变量说明示例
$0脚本名./script.sh
$1~$9位置参数第1~9个参数
$#参数个数3
$@所有参数(各自独立)"a" "b" "c"
$*所有参数(合为一个字符串)"a b c"
$?上一条命令的退出码0(成功)
$$当前脚本 PID12345
$!最近后台进程 PID12346
$_上一条命令的最后一个参数-
BASH
#!/usr/bin/env bash
echo "脚本名: $0"
echo "参数个数: $#"
echo "所有参数: $@"
echo "第一个参数: $1"
echo "第二个参数: $2"

# 运行:./script.sh foo bar baz
# 脚本名: ./script.sh
# 参数个数: 3
# 所有参数: foo bar baz
# 第一个参数: foo
# 第二个参数: bar

2.4 数组

BASH
#!/usr/bin/env bash

# 定义数组
fruits=("apple" "banana" "cherry" "date")

# 访问元素
echo ${fruits[0]}   # apple
echo ${fruits[2]}   # cherry

# 所有元素
echo ${fruits[@]}   # apple banana cherry date

# 数组长度
echo ${#fruits[@]}  # 4

# 遍历数组
for fruit in "${fruits[@]}"; do
  echo "水果: $fruit"
done

# 追加元素
fruits+=("elderberry")

# 删除元素
unset fruits[1]  # 删除 banana

# 关联数组(Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
config[debug]="true"

echo "主机: ${config[host]}"
echo "端口: ${config[port]}"

# 遍历关联数组
for key in "${!config[@]}"; do
  echo "${key} = ${config[$key]}"
done

三、条件判断

3.1 if 语句

BASH
#!/usr/bin/env bash

# 基本 if
if [ "$1" == "hello" ]; then
  echo "你好!"
fi

# if-else
if [ $# -eq 0 ]; then
  echo "没有提供参数"
else
  echo "参数个数: $#"
fi

# if-elif-else
if [ "$1" == "start" ]; then
  echo "启动服务..."
elif [ "$1" == "stop" ]; then
  echo "停止服务..."
elif [ "$1" == "restart" ]; then
  echo "重启服务..."
else
  echo "用法: $0 {start|stop|restart}"
  exit 1
fi

3.2 条件测试

字符串测试:

BASH
[ "$str" = "hello" ]      # 相等
[ "$str" != "hello" ]     # 不等
[ -z "$str" ]             # 为空
[ -n "$str" ]             # 不为空
[[ "$str" == h* ]]        # 通配符匹配(Bash 扩展)
[[ "$str" =~ ^[0-9]+$ ]]  # 正则匹配(Bash 扩展)

数字测试:

BASH
[ $num -eq 10 ]   # 等于
[ $num -ne 10 ]   # 不等于
[ $num -lt 10 ]   # 小于
[ $num -le 10 ]   # 小于等于
[ $num -gt 10 ]   # 大于
[ $num -ge 10 ]   # 大于等于

文件测试:

BASH
[ -f file.txt ]      # 是普通文件且存在
[ -d /path/dir ]     # 是目录且存在
[ -e file.txt ]      # 存在(任何类型)
[ -r file.txt ]      # 可读
[ -w file.txt ]      # 可写
[ -x file.txt ]      # 可执行
[ -s file.txt ]      # 文件大小 > 0
[ -L symlink ]       # 是符号链接
[ -f file1 -nt file2 ]  # file1 比 file2 新
[ -f file1 -ot file2 ]  # file1 比 file2 旧

逻辑运算:

BASH
# 传统写法
if [ -f file.txt ] && [ -r file.txt ]; then
  echo "文件存在且可读"
fi

# Bash 扩展写法(推荐,支持 && 和 ||)
if [[ -f file.txt && -r file.txt ]]; then
  echo "文件存在且可读"
fi

# 或
if [ -f file.txt ] || [ -d dir ]; then
  echo "文件或目录存在"
fi

3.3 case 语句

BASH
#!/usr/bin/env bash

case "$1" in
  start)
    echo "启动服务..."
    systemctl start myservice
    ;;
  stop)
    echo "停止服务..."
    systemctl stop myservice
    ;;
  restart)
    echo "重启服务..."
    systemctl restart myservice
    ;;
  status)
    systemctl status myservice
    ;;
  *)
    echo "用法: $0 {start|stop|restart|status}"
    exit 1
    ;;
esac

3.4 三元运算符

BASH
#!/usr/bin/env bash

# 条件 && 真值 || 假值
result=$([ $age -ge 18 ] && echo "成年" || echo "未成年")
echo $result

四、循环

4.1 for 循环

BASH
#!/usr/bin/env bash

# 遍历列表
for fruit in apple banana cherry; do
  echo "水果: $fruit"
done

# 遍历数字范围
for i in {1..5}; do
  echo "数字: $i"
done

# 带步长
for i in {0..10..2}; do
  echo "偶数: $i"
done

# C 风格 for 循环
for ((i=0; i<5; i++)); do
  echo "索引: $i"
done

# 遍历文件
for file in /etc/*.conf; do
  echo "配置文件: $file"
done

# 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
  echo "用户: $user"
done

4.2 while 循环

BASH
#!/usr/bin/env bash

# 基本循环
count=0
while [ $count -lt 5 ]; do
  echo "计数: $count"
  ((count++))
done

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

# 无限循环
while true; do
  echo "运行中..."
  sleep 1
done

# 按条件退出
while true; do
  read -p "继续吗?(y/n): " answer
  if [[ "$answer" != "y" ]]; then
    break
  fi
done

4.3 until 循环

BASH
#!/usr/bin/env bash

# 直到条件为真才停止
count=0
until [ $count -ge 5 ]; do
  echo "计数: $count"
  ((count++))
done

# 等待服务启动
until curl -s http://localhost:8080/health > /dev/null; do
  echo "等待服务启动..."
  sleep 2
done
echo "服务已启动!"

4.4 循环控制

BASH
#!/usr/bin/env bash

# break - 跳出循环
for i in {1..10}; do
  if [ $i -eq 5 ]; then
    break
  fi
  echo "数字: $i"
done

# continue - 跳过本次
for i in {1..10}; do
  if [ $((i % 2)) -eq 0 ]; then
    continue  # 跳过偶数
  fi
  echo "奇数: $i"
done

五、函数

5.1 定义与调用

BASH
#!/usr/bin/env bash

# 基本函数
greet() {
  echo "Hello, $1!"
}

# 调用
greet "Alice"
greet "Bob"

# 带返回值的函数
is_even() {
  local num=$1
  if [ $((num % 2)) -eq 0 ]; then
    return 0  # true
  else
    return 1  # false
  fi
}

# 使用函数返回值
if is_even 4; then
  echo "4 是偶数"
fi

# 函数输出捕获
get_date() {
  date '+%Y-%m-%d'
}
today=$(get_date)
echo "今天是: $today"

5.2 函数参数

BASH
#!/usr/bin/env bash

# 函数内 $1, $2... 是函数参数,不是脚本参数
add() {
  local a=$1
  local b=$2
  local sum=$((a + b))
  echo $sum
}

result=$(add 10 20)
echo "10 + 20 = $result"

# 多参数函数
create_user() {
  local username=$1
  local shell=$2
  local home_dir="/home/${username}"
  
  echo "创建用户: $username"
  echo "Shell: $shell"
  echo "Home: $home_dir"
  
  useradd -m -s "$shell" "$username"
  echo "用户 $username 创建成功"
}

create_user "deploy" "/bin/bash"

5.3 局部变量

BASH
#!/usr/bin/env bash

# 全局变量(函数内外都能访问)
global_var="I am global"

my_func() {
  # 局部变量(只在函数内有效)
  local local_var="I am local"
  
  echo "函数内 - global: $global_var"
  echo "函数内 - local: $local_var"
}

my_func
echo "函数外 - global: $global_var"
echo "函数外 - local: $local_var"  # 空,因为 local_var 只在函数内有效

重要:函数内一定要用 local 声明变量,避免污染全局作用域!


六、文本处理三剑客

6.1 grep — 文本搜索

BASH
#!/usr/bin/env bash

# 基本搜索
grep "error" /var/log/syslog

# 忽略大小写
grep -i "error" /var/log/syslog

# 反向匹配(不包含 error 的行)
grep -v "error" /var/log/syslog

# 显示行号
grep -n "error" /var/log/syslog

# 只显示匹配的部分
grep -o "ERROR: .*" /var/log/syslog

# 统计匹配行数
grep -c "error" /var/log/syslog

# 递归搜索目录
grep -r "TODO" /project/src/

# 只搜索特定文件
grep "error" /var/log/syslog --include="*.log"

# 使用正则表达式
grep -E "error|warning|critical" /var/log/syslog
grep -E "^2026-07" /var/log/syslog  # 以日期开头
grep -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" /var/log/syslog  # IP地址

# 上下文
grep -B 2 "error" /var/log/syslog   # 匹配行前2行
grep -A 2 "error" /var/log/syslog   # 匹配行后2行
grep -C 2 "error" /var/log/syslog   # 匹配行前后各2行

# 只输出文件名
grep -l "error" /var/log/*.log

6.2 sed — 流编辑器

BASH
#!/usr/bin/env bash

# 替换(每行第一个匹配)
sed 's/old/new/' file.txt

# 全局替换
sed 's/old/new/g' file.txt

# 忽略大小写替换
sed 's/old/new/gi' file.txt

# 只替换特定行
sed '5s/old/new/' file.txt        # 第5行
sed '5,10s/old/new/g' file.txt    # 第5-10行
sed '/pattern/s/old/new/g' file.txt  # 匹配 pattern 的行

# 删除行
sed '/^$/d' file.txt              # 删除空行
sed '/^#/d' file.txt              # 删除注释行
sed '1,3d' file.txt               # 删除1-3行
sed '$d' file.txt                 # 删除最后一行

# 插入和追加
sed '1i\Header Line' file.txt     # 在第1行前插入
sed '$a\Footer Line' file.txt     # 在最后行后追加

# 原地修改文件
sed -i 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.txt  # 保留备份

# 多个命令
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt

# 使用 & 引用匹配内容
sed 's/[0-9]\+/[&]/g' file.txt  # 数字加方括号: 123 → [123]

# 使用捕获组
echo "2026-07-11" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\1年\2月\3日/'
# 输出: 2026年07月11日

6.3 awk — 文本分析

BASH
#!/usr/bin/env bash

# 基本用法:按列提取
awk '{print $1}' file.txt          # 第一列
awk '{print $1, $3}' file.txt      # 第一列和第三列
awk '{print $NF}' file.txt         # 最后一列
awk '{print $(NF-1)}' file.txt     # 倒数第二列

# 指定分隔符
awk -F: '{print $1}' /etc/passwd   # 用户名
awk -F: '{print $1, $7}' /etc/passwd  # 用户名和Shell
awk -F',' '{print $2}' data.csv    # CSV 第二列

# 条件过滤
awk '$3 > 100' file.txt            # 第三列大于100的行
awk '$1 == "ERROR"' file.txt       # 第一列等于ERROR
awk '/error/' file.txt             # 包含error的行
awk '/2026-07/ && /error/' file.txt  # 同时满足两个条件

# 内置变量
awk '{print NR, NF, $0}' file.txt
# NR - 行号
# NF - 列数
# $0 - 整行

# BEGIN 和 END
awk 'BEGIN {print "开始处理"} {print $1} END {print "处理完成"}' file.txt

# 统计行数
awk 'END {print NR}' file.txt

# 求和
awk '{sum += $1} END {print sum}' file.txt

# 求平均值
awk '{sum += $1} END {print sum/NR}' file.txt

# 按列分组统计
awk '{count[$1]++} END {for (key in count) print key, count[key]}' file.txt

# 格式化输出
awk '{printf "%-10s %5d\n", $1, $2}' file.txt

# 实战:统计日志中各 IP 的访问次数(取前10)
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

6.4 其他常用文本工具

BASH
# sort - 排序
sort file.txt                # 升序
sort -r file.txt             # 降序
sort -n file.txt             # 数字排序
sort -u file.txt             # 去重排序
sort -t: -k3n /etc/passwd   # 按第3列数字排序

# uniq - 去重
sort file.txt | uniq         # 去重
sort file.txt | uniq -c      # 去重并计数
sort file.txt | uniq -d      # 只显示重复行

# cut - 切分
cut -d: -f1 /etc/passwd     # 按冒号切分,取第1列
cut -c1-10 file.txt         # 取每行第1-10个字符

# tr - 字符替换
echo "HELLO" | tr 'A-Z' 'a-z'           # 大写转小写
echo "hello world" | tr ' ' '-'         # 空格转横线
cat file.txt | tr -d '\r'               # 删除 Windows 换行符
echo "12345" | tr -d '0-9'              # 删除数字

# paste - 合并文件
paste file1.txt file2.txt              # 按列合并
paste -d',' file1.txt file2.txt        # 用逗号分隔

# wc - 统计
wc -l file.txt               # 行数
wc -w file.txt               # 单词数
wc -c file.txt               # 字符数

# find - 查找文件
find /var/log -name "*.log"              # 按名查找
find /var/log -name "*.log" -mtime -7    # 7天内修改的
find /var/log -name "*.log" -size +100M  # 大于100MB的
find /tmp -type f -delete                # 删除找到的文件
find . -name "*.js" -exec chmod 644 {} +  # 对找到的文件执行命令

# xargs - 参数传递
find . -name "*.log" | xargs grep "error"
find . -name "*.bak" | xargs rm -f
cat urls.txt | xargs -I{} curl -s {}

七、输入输出与重定向

7.1 重定向

BASH
#!/usr/bin/env bash

# 标准输出重定向
echo "hello" > file.txt      # 覆盖
echo "world" >> file.txt     # 追加

# 标准错误重定向
command 2> error.log         # 只重定向错误
command 2>> error.log        # 追加错误

# 标准输出和错误都重定向
command > all.log 2>&1       # 传统写法
command &> all.log           # Bash 4+ 简写

# 丢弃输出
command > /dev/null 2>&1

# 标准输入重定向
while read line; do
  echo "$line"
done < file.txt

# Here Document(多行文本)
cat << 'EOF' > config.txt
server:
  host: localhost
  port: 8080
EOF

# Here String
grep "pattern" <<< "search in this string"

7.2 管道

BASH
#!/usr/bin/env bash

# 命令管道
cat /etc/passwd | grep "/bin/bash" | cut -d: -f1

# 统计 Nginx 访问日志中状态码分布
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# 找出最大的10个文件
du -ah /var/log | sort -rh | head -10

# 查找并终止进程
ps aux | grep nginx | grep -v grep | awk '{print $2}' | xargs kill

# 使用 tee 同时输出到屏幕和文件
command | tee output.log
command | tee -a output.log  # 追加模式

八、实用运维脚本

8.1 系统监控脚本

BASH
#!/usr/bin/env bash
# system_monitor.sh - 系统资源监控

set -euo pipefail

echo "======================================"
echo "       系统资源监控报告"
echo "======================================"
echo ""

# CPU 信息
echo "【CPU 使用率】"
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}')
echo "  CPU: ${cpu_usage}%"
echo ""

# 内存信息
echo "【内存使用】"
free -h | awk '/^Mem:/ {printf "  总计: %s\n  已用: %s (%.1f%%)\n  可用: %s\n", $2, $3, ($3/$2)*100, $7}'
echo ""

# 磁盘使用
echo "【磁盘使用】"
df -h | awk 'NR==1 || /^\/dev\// {printf "  %-20s %6s %6s %6s %s\n", $1, $2, $3, $5, $6}'
echo ""

# 网络连接
echo "【网络连接统计】"
echo "  ESTABLISHED: $(ss -s | grep -o 'estab=[0-9]*' | cut -d= -f2)"
echo "  TIME_WAIT:   $(ss -ant | grep -c TIME-WAIT)"
echo "  LISTEN:      $(ss -ant | grep -c LISTEN)"
echo ""

# 负载均衡
echo "【系统负载】"
uptime | awk -F'load average:' '{print "  负载:" $2}'
echo ""

# Top 5 CPU 进程
echo "【CPU 占用 Top 5】"
ps aux --sort=-%cpu | head -6 | awk '{printf "  %-10s %6s%% %s\n", $1, $3, $11}'
echo ""

# Top 5 内存进程
echo "【内存占用 Top 5】"
ps aux --sort=-%mem | head -6 | awk '{printf "  %-10s %6s%% %s\n", $1, $4, $11}'
echo ""

echo "======================================"
echo "  报告生成时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "======================================"

8.2 日志分析脚本

BASH
#!/usr/bin/env bash
# log_analyzer.sh - Nginx 日志分析

set -euo pipefail

LOG_FILE="${1:-/var/log/nginx/access.log}"

if [ ! -f "$LOG_FILE" ]; then
  echo "错误: 日志文件 $LOG_FILE 不存在"
  exit 1
fi

echo "======================================"
echo "   Nginx 日志分析报告"
echo "   日志文件: $LOG_FILE"
echo "======================================"
echo ""

# 总请求数
total=$(wc -l < "$LOG_FILE")
echo "【总请求数】 $total"
echo ""

# 状态码分布
echo "【状态码分布】"
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn | while read count code; do
  printf "  %s: %d (%.1f%%)\n" "$code" "$count" "$(echo "scale=1; $count * 100 / $total" | bc)"
done
echo ""

# Top 10 IP
echo "【访问 IP Top 10】"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10 | while read count ip; do
  printf "  %-15s %8d 次\n" "$ip" "$count"
done
echo ""

# Top 10 请求路径
echo "【请求路径 Top 10】"
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10 | while read count path; do
  printf "  %-40s %8d 次\n" "$path" "$count"
done
echo ""

# 404 错误页面
echo "【404 错误页面 Top 10】"
awk '$9 == 404 {print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10 | while read count path; do
  printf "  %-40s %8d 次\n" "$path" "$count"
done
echo ""

# 流量统计
echo "【流量统计】"
awk '{sum += $10} END {printf "  总流量: %.2f GB\n", sum/1024/1024/1024}' "$LOG_FILE"
echo ""

# 按小时分布
echo "【按小时请求分布】"
awk '{print $4}' "$LOG_FILE" | cut -c14-15 | sort | uniq -c | while read count hour; do
  printf "  %s时: %d\n" "$hour" "$count"
done

8.3 自动备份脚本

BASH
#!/usr/bin/env bash
# backup.sh - 网站数据自动备份

set -euo pipefail

# ===== 配置 =====
SOURCE_DIR="/var/www/mysite"
BACKUP_DIR="/backup"
REMOTE_DIR="user@backup-server:/backup"
RETENTION_DAYS=7
MYSQL_DB="mysite"
MYSQL_USER="root"
MYSQL_PASS="${DB_PASSWORD}"
LOG_FILE="/var/log/backup.log"

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

send_alert() {
  # 发送告警邮件或 Telegram 通知
  curl -s "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \
    -d "chat_id=${TG_CHAT_ID}" \
    -d "text=备份告警: $1" > /dev/null
}

# ===== 主流程 =====
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
BACKUP_NAME="mysite_${TIMESTAMP}"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}"

log "开始备份: ${BACKUP_NAME}"

# 创建备份目录
mkdir -p "$BACKUP_PATH"

# 1. 备份文件
log "备份网站文件..."
if tar -czf "${BACKUP_PATH}/files.tar.gz" -C "$SOURCE_DIR" .; then
  log "文件备份完成: $(du -sh "${BACKUP_PATH}/files.tar.gz" | cut -f1)"
else
  log "文件备份失败!"
  send_alert "网站文件备份失败"
  exit 1
fi

# 2. 备份数据库
log "备份数据库..."
if mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASS" "$MYSQL_DB" | gzip > "${BACKUP_PATH}/db.sql.gz"; then
  log "数据库备份完成: $(du -sh "${BACKUP_PATH}/db.sql.gz" | cut -f1)"
else
  log "数据库备份失败!"
  send_alert "数据库备份失败"
  exit 1
fi

# 3. 生成校验文件
cd "$BACKUP_DIR"
md5sum "${BACKUP_NAME}"/* > "${BACKUP_NAME}/checksum.md5"
log "校验文件已生成"

# 4. 打包总文件
tar -czf "${BACKUP_NAME}.tar.gz" "$BACKUP_NAME"
log "总备份文件: $(du -sh "${BACKUP_NAME}.tar.gz" | cut -f1)"

# 5. 上传到远程
log "上传到远程服务器..."
if rsync -az --progress "${BACKUP_NAME}.tar.gz" "$REMOTE_DIR/"; then
  log "远程上传完成"
else
  log "远程上传失败!"
  send_alert "远程备份上传失败"
fi

# 6. 清理旧备份
log "清理 ${RETENTION_DAYS} 天前的旧备份..."
find "$BACKUP_DIR" -name "mysite_*.tar.gz" -mtime +${RETENTION_DAYS} -delete
find "$BACKUP_DIR" -type d -name "mysite_*" -mtime +${RETENTION_DAYS} -exec rm -rf {} +

# 清理远程旧备份
ssh user@backup-server "find /backup -name 'mysite_*.tar.gz' -mtime +${RETENTION_DAYS} -delete"

log "备份完成!"

8.4 服务部署脚本

BASH
#!/usr/bin/env bash
# deploy.sh - 自动化部署脚本

set -euo pipefail

# ===== 配置 =====
APP_NAME="myapp"
APP_DIR="/opt/${APP_NAME}"
REPO_URL="git@github.com:user/myapp.git"
BRANCH="main"
PID_FILE="/var/run/${APP_NAME}.pid"
LOG_FILE="/var/log/${APP_NAME}.log"

# ===== 函数 =====
log() {
  echo -e "\033[32m[$(date '+%H:%M:%S')]\033[0m $1"
}

error() {
  echo -e "\033[31m[错误]\033[0m $1"
  exit 1
}

check_service() {
  if systemctl is-active --quiet "$APP_NAME"; then
    return 0
  else
    return 1
  fi
}

# ===== 主流程 =====
log "开始部署 ${APP_NAME}..."

# 1. 拉取最新代码
log "拉取最新代码..."
if [ -d "$APP_DIR/.git" ]; then
  cd "$APP_DIR"
  git fetch origin
  git reset --hard "origin/${BRANCH}"
else
  git clone -b "$BRANCH" "$REPO_URL" "$APP_DIR"
  cd "$APP_DIR"
fi

log "当前版本: $(git rev-parse --short HEAD)"

# 2. 安装依赖
log "安装依赖..."
if [ -f "package.json" ]; then
  pnpm install --frozen-lockfile
elif [ -f "requirements.txt" ]; then
  pip install -r requirements.txt
elif [ -f "go.mod" ]; then
  go mod download
fi

# 3. 构建
log "构建项目..."
if [ -f "package.json" ]; then
  pnpm build
elif [ -f "Makefile" ]; then
  make build
fi

# 4. 运行测试
log "运行测试..."
if [ -f "package.json" ]; then
  pnpm test
fi

# 5. 重启服务
log "重启服务..."
if check_service; then
  systemctl restart "$APP_NAME"
  log "服务已重启"
else
  systemctl start "$APP_NAME"
  log "服务已启动"
fi

# 6. 健康检查
log "健康检查..."
sleep 3
for i in {1..10}; do
  if curl -sf "http://localhost:8080/health" > /dev/null; then
    log "健康检查通过!"
    log "部署成功!版本: $(git rev-parse --short HEAD)"
    exit 0
  fi
  log "等待服务就绪... ($i/10)"
  sleep 2
done

error "健康检查失败!请检查日志: $LOG_FILE"

8.5 批量服务器管理

BASH
#!/usr/bin/env bash
# multi_server.sh - 批量管理多台服务器

set -euo pipefail

# 服务器列表
SERVERS=(
  "web01:192.168.1.10"
  "web02:192.168.1.11"
  "db01:192.168.1.20"
  "cache01:192.168.1.30"
)

SSH_USER="admin"
SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no"

# 在所有服务器上执行命令
run_on_all() {
  local cmd="$1"
  for server in "${SERVERS[@]}"; do
    local name="${server%%:*}"
    local ip="${server##*:}"
    echo "--- ${name} (${ip}) ---"
    ssh $SSH_OPTS "${SSH_USER}@${ip}" "$cmd" 2>&1
    echo ""
  done
}

# 查看所有服务器状态
status() {
  run_on_all 'uptime && free -h | grep Mem && df -h / | tail -1'
}

# 在所有服务器上更新系统
update_all() {
  run_on_all 'sudo apt update && sudo apt upgrade -y'
}

# 在所有服务器上重启某服务
restart_service() {
  local service="$1"
  run_on_all "sudo systemctl restart $service && sudo systemctl status $service"
}

# 拷贝文件到所有服务器
distribute_file() {
  local file="$1"
  local dest="$2"
  for server in "${SERVERS[@]}"; do
    local name="${server%%:*}"
    local ip="${server##*:}"
    echo "拷贝到 ${name} (${ip})..."
    scp $SSH_OPTS "$file" "${SSH_USER}@${ip}:${dest}"
  done
}

# 主菜单
case "${1:-}" in
  status)    status ;;
  update)    update_all ;;
  restart)   restart_service "${2:?需要指定服务名}" ;;
  distribute) distribute_file "${2:?需要指定源文件}" "${3:?需要指定目标路径}" ;;
  exec)      run_on_all "${2:?需要指定命令}" ;;
  *)
    echo "用法: $0 {status|update|restart <service>|distribute <file> <dest>|exec <command>}"
    exit 1
    ;;
esac

九、systemd 服务管理

9.1 创建 systemd 服务

INI
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application Service
Documentation=https://github.com/user/myapp
After=network.target mysql.service
Wants=mysql.service

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/dist/index.js
ExecStop=/bin/kill -TERM $MAINPID
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000

# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# 安全限制
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data

# 资源限制
LimitNOFILE=65536
MemoryMax=512M
CPUQuota=100%

[Install]
WantedBy=multi-user.target

9.2 管理命令

BASH
# 重新加载 systemd 配置
sudo systemctl daemon-reload

# 启动/停止/重启
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp

# 查看状态
sudo systemctl status myapp

# 开机自启
sudo systemctl enable myapp
sudo systemctl disable myapp

# 查看日志
journalctl -u myapp              # 所有日志
journalctl -u myapp -f           # 实时跟踪
journalctl -u myapp --since today
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -p err       # 只看错误

9.3 systemd 定时器(替代 Cron)

INI
# /etc/systemd/system/backup.service
[Unit]
Description=Backup Service

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
INI
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily Backup Timer

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
BASH
# 启用定时器
sudo systemctl enable backup.timer
sudo systemctl start backup.timer

# 查看定时器
systemctl list-timers

十、Cron 定时任务

10.1 Cron 配置

BASH
# 编辑 crontab
crontab -e

# 查看当前 crontab
crontab -l

# 格式:
# 分钟 小时 日期 月份 星期 命令
# *    *    *    *    *    command
#
# ┌───── 分钟 (0-59)
# │ ┌───── 小时 (0-23)
# │ │ ┌───── 日期 (1-31)
# │ │ │ ┌───── 月份 (1-12)
# │ │ │ │ ┌───── 星期 (0-7, 0和7都是周日)
# │ │ │ │ │
# * * * * * command

10.2 常用 Cron 示例

BASH
# 每天凌晨2点执行备份
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# 每5分钟检查服务
*/5 * * * * /opt/scripts/health-check.sh

# 每周一早上8点
0 8 * * 1 /opt/scripts/weekly-report.sh

# 每月1号执行
0 0 1 * * /opt/scripts/monthly-task.sh

# 工作日每小时执行
0 * * * 1-5 /opt/scripts/workday-task.sh

# 每天的 9:00, 12:00, 18:00
0 9,12,18 * * * /opt/scripts/check.sh

# 每30秒执行(Cron最小粒度是分钟)
* * * * * /opt/scripts/task.sh ; sleep 30 ; /opt/scripts/task.sh

# 环境变量
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
MAILTO=admin@example.com

0 2 * * * /opt/scripts/backup.sh

十一、脚本调试与最佳实践

11.1 调试技巧

BASH
# 1. 语法检查(不执行)
bash -n script.sh

# 2. 执行追踪(打印每条命令)
bash -x script.sh

# 3. 在脚本中开启/关闭调试
#!/usr/bin/env bash
set -x  # 开启调试
# ... 代码 ...
set +x  # 关闭调试

# 4. 检查特定行
set -x  # 从这里开始追踪
echo "debugging this part"
set +x  # 停止追踪

# 5. 使用 PS4 自定义调试前缀
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
bash -x script.sh

# 6. 临时输出变量值
echo "DEBUG: var=$var" >&2

11.2 最佳实践

BASH
#!/usr/bin/env bash
# ========================================
# 脚本名称: deploy.sh
# 描述: 自动化部署脚本
# 作者: Your Name
# 日期: 2026-07-11
# 版本: 1.0.0
# ========================================

set -euo pipefail
IFS=$'\n\t'

# ===== 配置 =====
readonly APP_NAME="myapp"
readonly APP_DIR="/opt/myapp"
readonly LOG_FILE="/var/log/deploy.log"

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

die() {
  echo "[FATAL] $*" >&2
  exit 1
}

cleanup() {
  log "清理临时文件..."
  rm -f /tmp/myapp_*
}
trap cleanup EXIT  # 脚本退出时自动清理

usage() {
  cat << EOF
用法: $0 [选项]

选项:
  -e, --env ENV      环境名 (dev/staging/prod)
  -v, --version VER  部署版本号
  -h, --help         显示帮助

示例:
  $0 --env prod --version v1.2.3
EOF
}

# ===== 参数解析 =====
while [[ $# -gt 0 ]]; do
  case "$1" in
    -e|--env)
      ENV="$2"
      shift 2
      ;;
    -v|--version)
      VERSION="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      die "未知参数: $1"
      ;;
  esac
done

# ===== 参数校验 =====
: "${ENV:?请指定环境 (--env)}"
: "${VERSION:?请指定版本 (--version)}"

[[ "$ENV" =~ ^(dev|staging|prod)$ ]] || die "环境必须是 dev/staging/prod"

# ===== 主流程 =====
log "开始部署 ${APP_NAME} ${VERSION}${ENV}..."
# ... 部署逻辑 ...
log "部署完成!"

11.3 安全注意事项

BASH
# 1. 永远引用变量
rm "$file"        # ✅ 安全
rm $file          # ❌ 危险(文件名有空格会出错)

# 2. 使用 [[ ]] 代替 [ ]
if [[ -f "$file" ]]; then   # ✅ 更安全
if [ -f "$file" ]; then     # 也可以,但不支持正则

# 3. 使用 read -r 防止反斜杠转义
while read -r line; do      # ✅
while read line; do         # ❌

# 4. 临时文件用 mktemp
tmpfile=$(mktemp)           # ✅ 安全
tmpfile="/tmp/myapp.$$"     # ❌ 可能被预测

# 5. 敏感信息用环境变量
DB_PASS="${DB_PASS:?请设置 DB_PASS 环境变量}"

# 6. 限制脚本权限
chmod 700 script.sh         # 只有 owner 能读写执行

# 7. 避免命令注入
# 如果用户输入作为命令的一部分,要小心
user_input="$(cat user_input.txt)"
# ❌ 危险
eval "$user_input"
# ✅ 安全
echo "$user_input" | bash

十二、常用脚本速查表

12.1 文件操作

BASH
# 批量重命名
for f in *.jpg; do mv "$f" "vacation_$f"; done

# 批量转换格式
for f in *.wav; do ffmpeg -i "$f" "${f%.wav}.mp3"; done

# 同步目录
rsync -avz --delete source/ dest/

# 查找大文件
find / -type f -size +500M -exec ls -lh {} \; 2>/dev/null

# 查找并删除空目录
find . -type d -empty -delete

# 比较两个目录
diff -rq dir1/ dir2/

12.2 进程管理

BASH
# 查找并终止进程
kill $(pgrep -f "node app.js")

# 等待进程结束
while pgrep -f "myapp" > /dev/null; do
  echo "等待进程结束..."
  sleep 1
done

# 限制进程 CPU
cpulimit -p 12345 -l 50  # 限制为50% CPU

# 查看进程打开的文件
lsof -p 12345

12.3 网络操作

BASH
# 检查端口是否开放
nc -zv host 80

# 批量检查多台服务器
for host in web01 web02 db01; do
  echo -n "$host: "
  nc -zv -w2 "$host" 22 2>&1 | grep -o "succeeded\|failed"
done

# 下载文件并校验
wget -q "https://example.com/file.tar.gz"
echo "abc123  file.tar.gz" | md5sum -c

# 批量下载 URL
while read url; do
  wget -q "$url"
done < urls.txt

12.4 数据库操作

BASH
# 备份所有数据库
mysqldump -u root -p --all-databases > all_db.sql

# 备份单个数据库并压缩
mysqldump -u root -p mydb | gzip > mydb_$(date +%Y%m%d).sql.gz

# 恢复数据库
gunzip < mydb_20260711.sql.gz | mysql -u root -p mydb

# 批量执行 SQL
mysql -u root -p -e "SELECT User, Host FROM mysql.user;"

# 检查数据库大小
mysql -u root -p -e "
  SELECT table_schema AS '数据库',
         ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS '大小 (MB)'
  FROM information_schema.tables
  GROUP BY table_schema;"

十三、总结

Shell 脚本学习路径

PLAINTEXT
第 1 周:基础
  → 变量、条件、循环、函数
  → 写简单的自动化脚本

第 2 周:文本处理
  → grep、sed、awk 熟练使用
  → 管道和重定向
  → 日志分析脚本

第 3-4 周:进阶
  → 数组、关联数组
  → 正则表达式
  → 函数库和复用
  → 参数解析

第 2 月:生产级
  → systemd 服务管理
  → Cron 定时任务
  → 错误处理和日志
  → 安全最佳实践

第 3 月+:高级
  → 批量服务器管理
  → CI/CD 集成
  → 监控告警脚本
  → 性能优化

必知必会清单



Shell 脚本是运维人员的超能力。 掌握它,你可以把重复的工作自动化,把复杂的操作一键化,把手动的过程标准化。写好 Shell 脚本,是成为高级运维工程师的必经之路。🚀

版权声明

作者: 易邦

链接: https://blog.e8k.net/posts/linux-shell-automation-guide-2026/

许可证: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。