第 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 "儿童"
fi5.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++))
done5.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 -c9.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 |
动手练习
- 写一个自动部署脚本
- 写一个日志分析脚本(nginx/access.log)
- 用 cron 定时备份 MySQL
- 写一个系统监控脚本 + 告警
推荐阅读
下一章:第 209 章:Nginx 入门与反向代理 →