楼层: 首页/ 软件技术/ Linux/ Shell 脚本编程(Bash)
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;把重复劳动写成脚本,是运维的第一生产力。