主题
Shell 脚本编程
Shell 脚本是把命令行技能放大的杠杆——一劳永逸地自动化重复操作。本篇覆盖从变量到生产级脚本的全部知识点。
基础语法
变量
bash
#!/bin/bash
# 变量赋值(等号两边不能有空格!)
name="production-server"
count=42
# 使用变量(推荐双引号包裹)
echo "Server: ${name}, Count: ${count}"
echo "Log path: /var/log/${name}/app.log" # 花括号划定边界
# 特殊变量
echo "脚本名: $0"
echo "第1个参数: $1, 第2个: $2"
echo "参数个数: $#"
echo "所有参数: $@"
echo "上一命令退出码: $?" # 0=成功, 非0=失败
echo "当前进程 PID: $$"字符串操作
bash
str="hello-world-backup-2026.tar.gz"
echo ${str%.tar.gz} # hello-world-backup-2026(去除最短匹配后缀)
echo ${str%%.*} # hello-world-backup-2026(去除最长匹配后缀)
echo ${str#*-} # world-backup-2026.tar.gz(去除最短匹配前缀)
echo ${str##*-} # 2026.tar.gz(去除最长匹配前缀)
echo ${str/world/universe} # hello-universe-backup-2026.tar.gz(替换)
echo ${#str} # 字符串长度
# 实战:提取日期用于备份命名
backup_file="db_2026-07-26.sql.gz"
date_part=${backup_file#*_} # 2026-07-26.sql.gz
date_part=${date_part%.sql.gz} # 2026-07-26数组
bash
servers=("web01" "web02" "db01" "redis01")
echo ${servers[0]} # web01
echo ${servers[@]} # 所有元素
echo ${#servers[@]} # 数组长度
# 遍历数组
for server in "${servers[@]}"; do
echo "Deploying to $server..."
done条件判断
if / test / [ ]
bash
# 三种写法等价
if test -f /etc/nginx/nginx.conf; then ... fi
if [ -f /etc/nginx/nginx.conf ]; then ... fi # [ ] 是 test 的别名
if [[ -f /etc/nginx/nginx.conf ]]; then ... fi # [[ ]] 是 bash 扩展,更强大
# 文件测试
[ -f file ] # 是否为普通文件
[ -d dir ] # 是否为目录
[ -e path ] # 是否存在
[ -r file ] # 是否可读
[ -w file ] # 是否可写
[ -x file ] # 是否可执行
[ -s file ] # 是否存在且非空
# 字符串测试
[ "$a" = "$b" ] # 相等(注意变量加引号!)
[ -z "$str" ] # 是否为空
[ -n "$str" ] # 是否非空
# 数值测试
[ "$n" -eq 5 ] # equal
[ "$n" -ne 5 ] # not equal
[ "$n" -gt 5 ] # greater than
[ "$n" -lt 5 ] # less than
[ "$n" -ge 5 ] # greater or equal
[ "$n" -le 5 ] # less or equal
# 逻辑运算
[ "$a" = "yes" ] && [ "$b" = "yes" ] # AND
[ "$a" = "yes" ] || [ "$b" = "yes" ] # OR
[ ! -f file ] # NOT[[ ]] vs [ ] — 到底用哪个?
bash
# [ ] 的问题:
[ $a = "hello" ] # 如果 $a 为空,会变成 [ = "hello" ],语法错!
[ "$a" = "hello" ] # 正确,但总是要加引号很烦
# [[ ]] 的优点:
[[ $a = "hello" ]] # 不加引号也安全
[[ $a =~ ^[0-9]+$ ]] # 支持正则匹配
[[ $a == hello && $b == world ]] # 支持 && ||建议
脚本里统一用 [[ ]],它更安全、功能更强。只有需要兼容 /bin/sh(非 bash)时才用 [ ]。
循环
bash
# for 循环(最常用)
for i in {1..5}; do
echo "Round $i"
done
for file in /var/log/*.log; do
echo "Processing $file"
gzip "$file"
done
# while 循环(读文件逐行处理)
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hosts
# 无限循环 + break
while true; do
if curl -s http://localhost:8080/health > /dev/null; then
echo "Service is up!"
break
fi
sleep 2
done
# case 分支
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart|reload)
echo "Restarting service..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac函数
bash
#!/bin/bash
# 函数定义
log_info() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}
# 带返回值的函数
check_port() {
local port=$1 # local = 局部变量,不影响外部
if ss -tlnp | grep -q ":$port "; then
return 0 # 成功(0=成功,非0=失败)
else
return 1
fi
}
# 调用
if check_port 8080; then
log_info "Port 8080 is listening"
else
log_error "Port 8080 is NOT listening"
fi调试三板斧
bash
#!/bin/bash
set -e # 任何命令失败(非零退出)立即退出
set -u # 使用未定义变量时报错退出
set -o pipefail # 管道中任何命令失败都算失败,而不仅最后一个
# 调试时加 -x,执行时打印每条命令
bash -x script.sh
# 或在脚本内:
set -x
# ... 需要调试的代码 ...
set +x生产脚本必须加
set -euo pipefail 这三个选项能拦截 90% 的脚本 bug,比事后排查高效得多。
实战脚本
1. 带日期+自动清理的备份脚本
bash
#!/bin/bash
set -euo pipefail
# === 配置 ===
BACKUP_DIR="/backup/mysql"
RETENTION_DAYS=7 # 保留最近 7 天
DATABASE="myapp"
DB_USER="backup"
DB_PASS="your_password"
# === 日期标记 ===
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DATABASE}_${DATE}.sql.gz"
# === 备份 ===
mkdir -p "$BACKUP_DIR"
mysqldump -u"$DB_USER" -p"$DB_PASS" "$DATABASE" | gzip > "$BACKUP_FILE"
echo "[OK] Backup saved: $BACKUP_FILE"
# === 清理旧备份 ===
find "$BACKUP_DIR" -name "${DATABASE}_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
echo "[OK] Removed backups older than ${RETENTION_DAYS} days"2. 批量重命名
bash
#!/bin/bash
# 将所有 .jpeg 改名为 .jpg
for file in *.jpeg; do
[ -e "$file" ] || continue # 没有匹配文件时跳过
mv "$file" "${file%.jpeg}.jpg"
echo "Renamed: $file → ${file%.jpeg}.jpg"
done3. CPU 监控告警脚本
bash
#!/bin/bash
set -euo pipefail
THRESHOLD=80
ALERT_EMAIL="ops@example.com"
CPU_USAGE=$(LC_ALL=C top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
CPU_USAGE=${CPU_USAGE%.*} # 取整数部分
if [ "$CPU_USAGE" -gt "$THRESHOLD" ]; then
MESSAGE="[ALERT] CPU usage is ${CPU_USAGE}% on $(hostname) at $(date)"
echo "$MESSAGE"
# 发送告警(根据实际情况选一种)
# echo "$MESSAGE" | mail -s "CPU Alert" "$ALERT_EMAIL"
# curl -X POST -d "$MESSAGE" https://your-webhook-url
fi常见坑
坑 1:变量不用双引号
bash
# 错误:文件名有空格会出问题
file="my document.txt"
rm $file # 实际执行 rm my document.txt,把两个文件都删了!
# 正确
rm "$file"变量引号规则
永远用双引号包裹变量,除非你明确知道不需要。唯一的例外是故意做 word splitting 的场景。
坑 2:空格敏感
bash
name = "hello" # 错误!两边有空格
name="hello" # 正确
if [ $a = "yes" ] # 错误!内部要有空格
if [ "$a" = "yes" ] # 正确:[ 是命令,后面需要空格坑 3:管道中的错误被忽略
bash
# 错误:即使 grep 失败,$? 也是 0(因为最后一个命令 echo 成功)
cat /etc/shadow | grep "bob" | echo "done"
# 正确:set -o pipefail
set -o pipefail
cat /etc/shadow | grep "bob" # 现在 grep 失败 → 脚本退出生产环境经验
- 脚本模板化:每个脚本都加
set -euo pipefail+ 日志函数 + 错误处理 - 幂等性:脚本要能安全地重复执行,不应因重复运行而出错
- 配置与逻辑分离:用变量或外部配置文件,别把 IP/密码硬编码在逻辑里
- 返回值检查:关键操作后检查
$?,别假设一切正常 - shellcheck:
sudo apt install shellcheck && shellcheck script.sh做静态检查
🎯 本章要点
$?是上一个命令的退出码(0=成功,非0=失败)- 变量赋值等号两边不能有空格
- 脚本开头加
set -euo pipefail捕获错误 - 用
[[ -f path ]]检查文件是否存在
加载练习题中...