阶段六 · Shell 脚本基础

常用脚本模式与实战

一句话总结

前两篇学了语法,这一篇把它们落地成真正能用的脚本:标准的参数解析,批量重命名,日志清理,服务健康检查,定时备份.
每个都是运维和开发中反复出现的真实场景,套用模板改改就能用.

模式一:标准参数解析

稍微正经一点的脚本都需要支持选项(如 -v,--help).手写解析的标准模式是 while + case + shift:

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

verbose=false
output="result.txt"

while [[ $# -gt 0 ]]; do # 还有参数没处理完就继续
case "$1" in
-v|--verbose)
verbose=true
shift # 消费掉这个参数,$2 变成新的 $1
;;
-o|--output)
output="$2" # 取下一个参数作为值
shift 2 # 一次消费两个(选项 + 值)
;;
-h|--help)
echo "用法: $0 [-v] [-o 文件] <输入>"
exit 0
;;
-*)
echo "未知选项: $1" >&2
exit 1
;;
*)
break # 不是选项了,剩下的是位置参数
;;
esac
done

echo "verbose=$verbose, output=$output, 剩余参数=$@"

shift 是关键

shift 把位置参数整体左移一位:原来的 $2 变成 $1,$# 减一.配合 while [[ $# -gt 0 ]] 循环,就能逐个"吃掉"参数.shift 2 一次移两位,用于"选项 + 值"成对出现的情况(如 -o result.txt).

复杂选项可以用 getopts

如果选项很多,bash 内置的 getopts 更省事,能自动处理 -vo file 这类合并写法.但它不支持长选项(--verbose).日常脚本用上面的 while+case 模式已经够灵活,需要长短选项混用时它反而更直观.

模式二:批量重命名文件

"把一堆文件按规则改名"是最经典的脚本场景.核心是 for 遍历 + 参数展开做字符串处理:

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

# 场景1:给当前目录所有 .jpeg 改成 .jpg
for f in *.jpeg; do
[[ -e "$f" ]] || continue # 没有匹配文件时跳过(避免处理字面量 *.jpeg)
mv -- "$f" "${f%.jpeg}.jpg" # %.jpeg 去掉结尾的 .jpeg
done

# 场景2:给所有 .log 文件加上日期前缀
today=$(date +%Y%m%d)
for f in *.log; do
[[ -e "$f" ]] || continue
mv -- "$f" "${today}_${f}"
done

# 场景3:把文件名里的空格替换成下划线
for f in *\ *; do # 匹配名字含空格的文件
[[ -e "$f" ]] || continue
mv -- "$f" "${f// /_}" # // 全局替换空格为下划线
done

这里用到几个参数展开技巧,是脚本里处理字符串的利器:

语法 作用 例(f=photo.jpeg)
${f%.jpeg} 从结尾删掉最短匹配 photo
${f%.*} 去掉扩展名 photo
${f##*.} 只取扩展名(从开头删到最后一个点) jpeg
${f//a/b} 把所有 a 替换成 b -
${f^^} / ${f,,} 转大写 / 转小写 -

批量操作先 echo 预演,再真删真改

批量 mv/rm 一旦写错规则,可能瞬间毁掉一整个目录.动手前先把 mv 换成 echo mv 跑一遍,确认打印出来的命令都对,再去掉 echo 真正执行.mv -- 里的 -- 表示"选项到此为止",防止文件名以 - 开头时被误当成选项.

模式三:日志清理

服务器上的日志会无限增长撑爆磁盘.一个定时清理脚本通常做两件事:删除过期日志,压缩旧日志.主角是 find-mtime-exec:

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

LOG_DIR="/var/log/myapp"
DAYS_KEEP=30

# 删除 30 天前的 .log 文件
find "$LOG_DIR" -name "*.log" -type f -mtime +${DAYS_KEEP} -delete
# ↑ mtime +30:修改时间在 30 天以前

# 压缩 7 天前但还没删的日志(节省空间)
find "$LOG_DIR" -name "*.log" -type f -mtime +7 -exec gzip {} \;
# ↑ 对每个找到的文件执行 gzip
# {} 是文件名占位符,\; 表示命令结束

echo "[$(date +%F\ %T)] 日志清理完成"

-mtime 的 +N,N,-N 别记混

find 的时间过滤:-mtime +30 是"超过 30 天前"(更老),-mtime -7 是"最近 7 天内"(更新),-mtime 0 是"今天".清理老文件用 +N,这是最容易记反的地方--加号朝过去,减号朝现在.

模式四:服务健康检查

判断一个服务是否正常,并在异常时告警或自动重启,是运维脚本的核心场景.它把前面学的curl 状态码(第 15 篇)+ 退出码 + 条件判断串了起来:

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

URL="http://localhost:8080/health"
SERVICE="myapp"

# 取 HTTP 状态码(回顾第 15 篇)
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$URL" || echo "000")
# ↑ 连不上时不让脚本崩,给个 000

if [[ "$code" == "200" ]]; then
echo "[OK] $SERVICE 健康 (HTTP $code)"
exit 0
else
echo "[FAIL] $SERVICE 异常 (HTTP $code),尝试重启" >&2
systemctl restart "$SERVICE" # 自动重启
exit 1
fi

--max-time 5 防止 curl 在服务卡死时一直挂着;服务完全连不上时 curl 会非零退出,在 set -e 下会让脚本直接崩--所以加 || echo "000" 兜底,把"连不上"也变成一个可判断的状态码.这种"给失败一个确定的兜底值"的思路在健康检查里很常见.

模式五:定时备份

备份脚本通常是"打包 → 加时间戳 → 清理旧备份"三步,综合运用打包,命令替换,find 清理:

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

SRC="/data/myapp" # 要备份的目录
DEST="/backup" # 备份存放处
KEEP=7 # 保留最近 7 份

timestamp=$(date +%Y%m%d_%H%M%S)
archive="${DEST}/myapp_${timestamp}.tar.gz"

# 打包压缩
tar -czf "$archive" -C "$(dirname "$SRC")" "$(basename "$SRC")"
echo "已备份到 $archive"

# 只保留最近 KEEP 份,多余的删掉
ls -1t "${DEST}"/myapp_*.tar.gz | tail -n +$((KEEP + 1)) | xargs -r rm --
# ↑ -t 按时间排序,最新在前
# ↑ 从第 KEEP+1 个开始(即多出来的旧备份)
# ↑ 交给 rm 删除

tail -n +N 与 xargs -r 的小技巧

tail -n +8 表示"从第 8 行开始到末尾"(注意是加号),配合 ls -1t(最新在前)就能精准选出"第 8 份及以后的旧备份".xargs -r-r 表示"没有输入时就不执行",避免备份数量没超标时 rm 被空参数调用而报错.

让脚本定时跑:cron

健康检查,日志清理,备份这类脚本都需要定时自动执行,这就是 cron 的活.用 crontab -e 编辑定时任务:

# 编辑当前用户的定时任务
$ crontab -e

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

0 2 * * * /home/alice/backup.sh # 每天凌晨 2:00 备份
*/5 * * * * /home/alice/health_check.sh # 每 5 分钟健康检查一次
0 3 * * 0 /home/alice/log_cleanup.sh # 每周日凌晨 3:00 清理日志

cron 环境极简,脚本里别依赖 PATH

cron 执行时的环境变量几乎是空的,登录 Shell 里的 PATH,别名都不存在.所以 cron 跑的脚本里,命令最好写绝对路径(或在脚本开头自己 export PATH=...),否则会出现"手动跑没问题,cron 一跑就找不到命令"的经典坑.把脚本输出重定向到日志(>> /tmp/x.log 2>&1)也方便事后排查.

快速回顾

  • 参数解析:用 while [[ $# -gt 0 ]] + case + shift;shift 左移参数逐个消费,选项带值用 shift 2
  • 批量重命名:靠 for + 参数展开:${f%.ext} 去后缀 · ${f##*.} 取后缀 · ${f//a/b} 全局替换;先 echo 预演再执行
  • 日志清理:用 find -mtime +N -delete/-exec;+N 是更老,-N 是更近,别记反
  • 健康检查:curl 取状态码 + || echo "000" 兜底 + if 判断 + 异常重启
  • 备份:tar -czf 加时间戳打包 + ls -1t | tail -n +N | xargs -r rm 清理旧备份
  • cron(crontab -e)做定时调度,五个字段是 分 时 日 月 周
  • cron 环境极简:脚本里用绝对路径,重定向输出到日志,避免"手动行,定时不行"

动手练习

  1. 参数解析:写一个支持 -v/--verbose-o 文件 选项的脚本,用 while+case+shift 解析,打印解析结果
  2. 批量重命名:建几个 .jpeg 测试文件,写脚本批量改成 .jpg;先用 echo mv 预演确认无误再真改
  3. 空格替换:用 ${f// /_} 写一个把当前目录文件名中空格换成下划线的脚本
  4. find 时间过滤:用 find . -name "*.tmp" -mtime +7 列出 7 天前的临时文件(先不加 -delete,只查看)
  5. 健康检查:写一个健康检查脚本,用 curl 检测某网站状态码,200 打印 OK,否则打印 FAIL,并验证连不上时 || echo "000" 的兜底
  6. 备份脚本:写一个备份脚本,用 tar -czf 把某目录打包成带时间戳的归档
  7. cron 验证:用 crontab -e 加一条"每分钟把时间写进一个文件"的任务,过几分钟看文件内容,确认 cron 生效后删掉这条任务