05
Shell 脚本编程(Bash)
Bash Scripting
命令行敲一条是"手动挡",把一堆命令写成一个文件、让它自动跑,就是 Shell 脚本——运维自动化的根基。每天重复一百次的活,写成脚本跑一次。这章教你用 Bash 写出能干活的脚本。
变量:记住 $ 和引号的脾气
定义变量、引用、环境变量、位置参数
# 定义变量:等号两边不能有空格!引用要加 $
name="alice"
echo "你好,$name"
# 环境变量:export 出去,子进程也能看到
export APP_ENV="production"
# 位置参数:脚本跑的时候传进来的
# 用法 ./backup.sh data /tmp ,则 $1=data $2=/tmp
echo "第一个参数是 $1"
echo "一共传了 $# 个参数"
echo "全部参数:$@"
echo "上一条命令的退出码:$?"
引三种引号,区别很大
双引号 " ":里面的 $变量 会被替换。echo "你好 $name" 输出"你好 alice"。
单引号 ' ':原汁原味,什么都不替换。echo '你好 $name' 输出字面的"你好 $name"。
反引号 ` ` 或 $( ):命令替换——把命令的结果塞进去。now=$(date +%F) 把今天日期存进 now。推荐用 $( ),能嵌套、看得清。
条件、循环、函数:脚本的骨架
if 条件 / case / 两种循环 / 函数
# 条件测试:[ 两边必须有空格,-f 判断文件存在,-d 判断目录
if [ -f /etc/hosts ]; then
echo "hosts 文件在"
elif [ -d /etc ]; then
echo "/etc 目录在"
else
echo "都没了?"
fi
# 数字比较用 -eq -gt -lt;字符串用 = !=;[[ ]] 是加强版,更安全
if [[ $age -gt 18 ]]; then echo "成年"; fi
# for 循环:遍历
for file in *.log; do
gzip "$file"
done
# while 循环:读到文件每一行
while read line; do
echo "处理:$line"
done < users.txt
# 函数:定义、调用、local 局部变量
greet() {
local who="$1"
echo "你好,$who"
}
greet "小明"
流重定向与管道
> 覆盖写入、>> 追加写入、< 从文件读入、2> 把错误输出丢弃。经典:command > /dev/null 2>&1 = 什么都不打印(静默运行)。管道 | 把前一条的输出喂给后一条。
数组、case 与交互输入
Bash 数组:存一组值
servers=(web01 web02 db01) # 定义数组(注意无空格)
echo "${servers[0]}" # 取下标 0
echo "${#servers[@]}" # 数组长度
for s in "${servers[@]}"; do # 遍历
echo "处理 $s"
done
case:比一长串 if-elif 清爽
case "$1" in
start) echo "启动服务" ;;
stop) echo "停止服务" ;;
restart) echo "重启服务" ;;
*) echo "用法: $0 {start|stop|restart}" ;;
esac
read:和用户交互输入
read -p "请输入你的名字:" name
echo "你好,$name"
# exit 数字:脚本退出码;return:函数的返回码
脚本调试:bash -x 和 set -euo pipefail
脚本一改就报错?两件套:① 脚本第一行写 set -euo pipefail——出错立刻停、用了未定义变量就报错、管道中间失败也算失败,把很多隐形 bug 直接掐死;② 跑的时候 bash -x script.sh,它会把每一行实际执行的样子打印出来,逐行排查。
完整案例一:自动备份脚本
backup.sh —— 把网站目录打包,按日期命名,保留最近 7 天
#!/usr/bin/env bash
set -euo pipefail
SOURCE="/var/www/html"
DEST="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
FILE="$DEST/site_$DATE.tar.gz"
echo "开始备份 $SOURCE ..."
tar -czf "$FILE" -C /var/www html
# 删除 7 天前的旧备份
find "$DEST" -name "site_*.tar.gz" -mtime +7 -delete
echo "备份完成:$FILE"
exit 0
$ chmod +x backup.sh && ./backup.sh
开始备份 /var/www/html ...
备份完成:/backup/site_20260916_103000.tar.gz
完整案例二:系统监控脚本
monitor.sh —— 看磁盘/内存/负载,超阈值就报警
#!/usr/bin/env bash
set -euo pipefail
# 磁盘使用率(%,取根分区)
disk_used=$(df / | awk 'NR==2{print $5}' | tr -d '%')
mem_free=$(free -m | awk 'NR==2{print $4}')
if [[ $disk_used -gt 85 ]]; then
echo "[警告] 磁盘已用 ${disk_used}%,超过 85% 阈值"
fi
if [[ $mem_free -lt 200 ]]; then
echo "[警告] 可用内存只剩 ${mem_free}MB"
fi
uptime | awk '{print "当前负载:" $8}'
$ ./monitor.sh
[警告] 磁盘已用 91%,超过 85% 阈值
当前负载:0.15, 0.10, 0.05
完整案例三:批量文件处理
rename_jpg.sh —— 把目录里所有 .jpg 按"日期_序号"重命名
#!/usr/bin/env bash
set -euo pipefail
count=1
date_str=$(date +%Y%m%d)
for img in *.jpg; do
new_name=$(printf "photo_%s_%03d.jpg" "$date_str" $count)
echo "$img -> $new_name"
mv "$img" "$new_name"
count=$((count + 1))
done
echo "共重命名 $((count-1)) 个文件"
$ ./rename_jpg.sh
IMG_001.jpg -> photo_20260916_001.jpg
IMG_002.jpg -> photo_20260916_002.jpg
共重命名 2 个文件
本章小练习
练习 1:写一个脚本,接受一个文件名参数,如果文件不存在就打印"找不到"
#!/usr/bin/env bash; if [ ! -f "$1" ]; then echo "找不到 $1"; fi。注意 [ ! -f "$1" ] 的空格和引号。
练习 2:count.sh 接收若干个数字参数,输出它们的总和
sum=0; for n in "$@"; do sum=$((sum+n)); done; echo $sum。用 $@ 遍历所有位置参数。
练习 3:为什么 name = "alice"(等号两边有空格)会报错
Bash 把 name 当成命令名去找,找不到就报 command not found。赋值时等号两边绝对不能有空格。
练习 4:'$HOME' 和 "$HOME" 输出有什么不同
单引号原样输出 $HOME;双引号会展开成你的家目录路径。要变量替换用双引号。
练习 5:脚本最后 exit 1 是什么意思
退出码 0 表示成功,非 0(如 1)表示失败。别的脚本或 cron 可以靠 $? 判断它跑没跑成功。
记
本章小结
① 变量赋值等号两边无空格,引用加 $;双引号会替换、单引号原样、$( ) 做命令替换。
② 骨架就是 if / for / while / 函数;重定向 > 覆盖 >> 追加,管道 | 串联。
③ 上线脚本头加 set -euo pipefail,排错用 bash -x;把重复劳动写成脚本,是运维的第一生产力。