Skip to content
第 208 / 250 章运维⏱ 12 分钟阅读

第 208 章:Shell 脚本编程

学习目标

  • 掌握 Bash 脚本语法
  • 学会流程控制与函数
  • 编写实用运维脚本
  • 了解常用脚本模板

一、第一个脚本

bash
#!/bin/bash
# hello.sh

echo "Hello World!"
echo "当前时间: $(date)"
echo "当前用户: $(whoami)"
echo "脚本参数: $@"
bash
chmod +x hello.sh
./hello.sh arg1 arg2

二、变量

2.1 普通变量

bash
name="Tom"           # 注意:等号不能有空格
readonly PI=3.14      # 只读
unset name            # 删除

echo $name
echo "${name}_user"   # 推荐用花括号

2.2 环境变量

bash
export JAVA_HOME=/opt/java
export PATH=$PATH:$JAVA_HOME/bin

# 常见环境变量
$HOME        # 家目录
$USER        # 当前用户
$PWD         # 当前目录
$PATH        # 命令搜索路径
$0           # 脚本名
$1-$9        # 参数
$@           # 所有参数
$#           # 参数个数
$?           # 上条命令退出码
$$           # 当前 PID

三、字符串

bash
str="Hello World"

echo ${#str}             # 长度:11
echo ${str:0:5}          # 截取:Hello
echo ${str/World/Bash}   # 替换:Hello Bash

# 单引号 vs 双引号
echo "$str $name"   # 解析变量
echo '$str $name'   # 原样输出

四、数组

bash
arr=("a" "b" "c")

echo ${arr[0]}       # a
echo ${arr[@]}       # 所有
echo ${#arr[@]}      # 长度

arr+=("d")           # 追加

# 遍历
for item in "${arr[@]}"; do
  echo "$item"
done

五、流程控制

5.1 条件

bash
# if
if [ "$age" -ge 18 ]; then
  echo "成年"
elif [ "$age" -ge 12 ]; then
  echo "少年"
else
  echo "儿童"
fi

5.2 比较

bash
# 数字
[ "$a" -eq "$b" ]    # ==
[ "$a" -ne "$b" ]    # !=
[ "$a" -gt "$b" ]    # >
[ "$a" -lt "$b" ]    # <
[ "$a" -ge "$b" ]    # >=
[ "$a" -le "$b" ]    # <=

# 字符串
[ -z "$str" ]        # 空
[ -n "$str" ]        # 非空
[ "$a" = "$b" ]      # 相等
[ "$a" != "$b" ]     # 不等

# 文件
[ -f "$file" ]       # 普通文件
[ -d "$dir" ]        # 目录
[ -e "$path" ]       # 存在
[ -r "$file" ]       # 可读
[ -x "$file" ]       # 可执行

# 复合
[ "$a" -gt 0 ] && [ "$a" -lt 100 ]
[[ "$str" =~ ^[0-9]+$ ]]   # 正则

5.3 循环

bash
# for
for i in 1 2 3 4 5; do
  echo $i
done

for i in {1..10}; do
  echo $i
done

for ((i=1; i<=10; i++)); do
  echo $i
done

# while
count=0
while [ $count -lt 5 ]; do
  echo $count
  ((count++))
done

# until
count=0
until [ $count -ge 5 ]; do
  echo $count
  ((count++))
done

5.4 case

bash
case "$1" in
  start)
    echo "启动"
    ;;
  stop)
    echo "停止"
    ;;
  restart)
    echo "重启"
    ;;
  *)
    echo "用法: $0 {start|stop|restart}"
    exit 1
    ;;
esac

六、函数

bash
function greet() {
  local name=$1           # local 局部变量
  echo "Hello, $name"
}

greet "Tom"

# 返回值
function add() {
  return $(($1 + $2))
}

add 1 2
echo $?    # 3

# 标准输出返回
function get_date() {
  date +%Y-%m-%d
}

today=$(get_date)

七、输入输出

bash
# 读
read -p "请输入: " input
read -s password        # 静默

# 写
echo "hello"
printf "name=%s age=%d\n" "Tom" 18

# 重定向
ls > file.txt           # 覆盖
ls >> file.txt          # 追加
cmd 2>&1                # 错误到标准输出
cmd &> file.txt         # 都到文件

# /dev/null 丢弃
cmd > /dev/null 2>&1

八、错误处理

bash
set -e              # 任何命令失败立即退出
set -u              # 使用未定义变量报错
set -o pipefail     # 管道失败即失败
set -x              # 打印执行的命令(调试)

# trap 错误处理
trap 'echo "出错了: $?"' ERR

# 清理
trap 'rm -f /tmp/tempfile' EXIT

九、实战脚本

9.1 部署脚本

bash
#!/bin/bash
set -euo pipefail

APP_NAME="myapp"
APP_PORT=8080
APP_HOME="/opt/$APP_NAME"
BACKUP_DIR="/data/backup/$(date +%Y%m%d)"
GIT_REPO="git@github.com:user/myapp.git"

# 颜色
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

log() { echo -e "${GREEN}[$(date +'%H:%M:%S')]${NC} $1"; }
err() { echo -e "${RED}[$(date +'%H:%M:%S')] ERROR:${NC} $1"; exit 1; }

# 检查 root
[[ $EUID -eq 0 ]] || err "需要 root"

# 备份
log "备份旧版本..."
mkdir -p "$BACKUP_DIR"
[ -d "$APP_HOME" ] && cp -r "$APP_HOME" "$BACKUP_DIR/"

# 拉代码
log "拉取代码..."
cd "$APP_HOME"
git pull origin main

# 编译
log "打包..."
pnpm install --prod
pnpm run build

# 重启
log "重启服务..."
pm2 reload "$APP_NAME" || pm2 start ecosystem.config.js

log "部署完成!"

9.2 日志分析

bash
#!/bin/bash
# 分析 nginx 访问日志
LOG="/var/log/nginx/access.log"

echo "===== Top 10 IP ====="
awk '{print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10

echo "===== Top 10 URL ====="
awk '{print $7}' "$LOG" | sort | uniq -c | sort -rn | head -10

echo "===== 状态码统计 ====="
awk '{print $9}' "$LOG" | sort | uniq -c | sort -rn

echo "===== 4xx/5xx 错误 ====="
awk '$9 >= 400 {print $9, $7}' "$LOG" | sort | uniq -c

9.3 数据库备份

bash
#!/bin/bash
set -euo pipefail

DB_USER="admin"
DB_PASS="secret"
DB_NAME="myapp"
BACKUP_DIR="/data/backup/db"
DATE=$(date +%Y%m%d_%H%M%S)
RETAIN_DAYS=7

mkdir -p "$BACKUP_DIR"

# 备份
mysqldump -u"$DB_USER" -p"$DB_PASS" \
  --single-transaction --routines --triggers \
  "$DB_NAME" | gzip > "$BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz"

# 清理旧备份
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +$RETAIN_DAYS -delete

echo "备份完成: ${DB_NAME}_${DATE}.sql.gz"

9.4 监控脚本

bash
#!/bin/bash
# 监控 CPU / 内存 / 磁盘
ALERT_THRESHOLD=80

check() {
  local name=$1
  local value=$2

  if [ "$value" -gt "$ALERT_THRESHOLD" ]; then
    echo "[ALERT] $name 使用率 ${value}%"
    # 发告警:钉钉 / 邮件 / Slack
  else
    echo "[OK] $name 使用率 ${value}%"
  fi
}

# CPU
cpu=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | awk -F. '{print $1}')
check "CPU" "$cpu"

# 内存
mem=$(free | awk '/Mem:/ {printf "%.0f", $3/$2 * 100}')
check "内存" "$mem"

# 磁盘
disk=$(df / | awk '/\// {print $5}' | sed 's/%//')
check "磁盘" "$disk"

十、cron 定时任务

bash
crontab -e

# 格式: 分 时 日 月 周 命令
# 每分钟
* * * * * /script.sh

# 每天凌晨 3 点
0 3 * * * /backup.sh

# 每 5 分钟
*/5 * * * * /monitor.sh

# 工作日每 10 分钟(周一到周五)
*/10 * * * 1-5 /job.sh

十一、本章小结

特性说明
变量无类型,字符串
数组索引访问
函数可返回状态码或字符串
错误处理set -e / trap

动手练习

  1. 写一个自动部署脚本
  2. 写一个日志分析脚本(nginx/access.log)
  3. 用 cron 定时备份 MySQL
  4. 写一个系统监控脚本 + 告警

推荐阅读


下一章:第 209 章:Nginx 入门与反向代理

本站基于 VitePress 构建 · 由 Codebook 团队维护