Shell 脚本程序控制语句:脚本的决策与循环大脑

引言:让脚本学会思考与重复

想象一下,你是一个指挥官,正在给部队下达指令:

  1. 如果if)发现敌人,立即开火。
  2. 只要while)阵地上还有敌人,就继续攻击。
  3. 对弹药库里的每一种for)武器进行检查。
  4. 如果弹药耗尽,跳出break)当前战斗,执行撤退计划。
  5. 如果只是小股敌人,跳过continue)重武器,使用步枪解决。

如果没有这些控制指令,你的命令将是混乱且低效的。同样,Shell 脚本中的程序控制语句就是它的“决策大脑”和“循环引擎”,它让脚本不再是简单粗暴地顺序执行每一条命令,而是能够根据条件做出判断、重复执行任务、选择不同的执行路径。

掌握了控制语句,你的脚本就拥有了真正的“智能”,能够应对各种复杂场景。本文将带你深入浅出地学习 Shell 脚本中所有这些强大的控制结构。


一、条件判断:如果…就…(if/else)

条件判断是脚本最基础的分支能力,让它能够“审时度势”。

1.1 基础 if 语法

if [ 条件 ]; then
# 如果条件成立,就执行这里的命令
fi

示例:检查文件是否存在

#!/bin/bash

file="/path/to/somefile.txt"

if [ -f "$file" ]; then # -f 用于判断是否为文件且存在
echo "文件 $file 存在!"
fi

1.2 如果…就…否则…(if/else)

if [ 条件 ]; then
# 条件成立时执行
else
# 条件不成立时执行
fi

示例:判断用户输入

#!/bin/bash

read -p "请输入'y'或'n': " answer

if [ "$answer" = "y" ]; then
echo "你选择了 Yes。"
else
echo "你选择了 No 或输入了其他内容。"
fi

1.3 多条件判断(if/elif/else)

if [ 条件1 ]; then
# 条件1成立时执行
elif [ 条件2 ]; then
# 条件2成立时执行
else
# 所有条件都不成立时执行
fi

示例:成绩等级划分

#!/bin/bash

read -p "请输入你的分数 (0-100): " score

if [ "$score" -ge 90 ]; then # -ge 大于等于
echo "成绩优秀!"
elif [ "$score" -ge 70 ]; then
echo "成绩良好。"
elif [ "$score" -ge 60 ]; then
echo "及格了。"
else
echo "不及格,需要努力哦!"
fi

1.4 条件测试的基石:test 命令与 [ ]

Shell 中的条件判断依赖于 test 命令。[ condition ] 实际上是 test condition 的另一种写法(注意 [ 后和 ] 前必须有空格!)。

常用测试运算符:

类别 运算符 含义 示例
文件测试 -e file 文件/目录是否存在 [ -e "/tmp" ]
-f file 是否是文件 [ -f "log.txt" ]
-d file 是否是目录 [ -d "/home" ]
-r file 文件是否可读 [ -r "data.txt" ]
-w file 文件是否可写 [ -w "config.cfg" ]
-x file 文件是否可执行 [ -x "app.sh" ]
-s file 文件长度是否 > 0(非空) [ -s "output.log" ]
字符串比较 -z str 字符串长度是否为 0 [ -z "$var" ]
-n str 字符串长度是否非 0 [ -n "$var" ]
str1 = str2 字符串是否相等 [ "$a" = "$b" ]
str1 != str2 字符串是否不相等 [ "$a" != "$b" ]
算术比较 num1 -eq num2 等于 (Equal) [ 5 -eq 5 ]
num1 -ne num2 不等于 (Not Equal) [ 5 -ne 3 ]
num1 -gt num2 大于 (Greater Than) [ 10 -gt 5 ]
num1 -lt num2 小于 (Less Than) [ 1 -lt 4 ]
num1 -ge num2 大于等于 (Greater or Equal) [ 5 -ge 5 ]
num1 -le num2 小于等于 (Less or Equal) [ 3 -le 5 ]
逻辑操作 ! 非 (NOT) [ ! -f "$file" ]
-a&& 与 (AND) [ "$a" -gt 0 -a "$a" -lt 10 ]
[[ $a -gt 0 && $a -lt 10 ]]
-o|| 或 (OR) [ "$ans" = "y" -o "$ans" = "Y" ]
[[ $ans = "y" || $ans = "Y" ]]

注意&&|| 通常在更强大的 [[ ]] 条件结构中使用,它比 [ ] 更通用,且能防止一些逻辑错误。


二、分支选择:情况…(case)

当需要根据一个变量的多种不同值来执行不同操作时,case 语句比一堆 elif 更清晰、更高效。

2.1 case 语法结构

case $变量 in
模式1)
# 如果变量值匹配模式1,执行这里
;;
模式2 | 模式3) # 匹配模式2或模式3
# 执行这里
;;
*) # 默认情况,匹配任何其他值
# 执行这里
;;
esac # case 反过来写,表示结束

示例:简单的命令解析器

#!/bin/bash

read -p "请输入命令 (start|stop|restart|status): " cmd

case $cmd in
start | Begin) # 匹配 "start" 或 "Begin"
echo "服务启动中..."
;;
stop | End)
echo "服务停止中..."
;;
restart)
echo "服务重启中..."
;;
status)
echo "服务状态:运行中..."
;;
*) # 如果输入了上面未列出的命令
echo "错误:未知命令 '$cmd'"
echo "可用命令: start, stop, restart, status"
exit 1
;;
esac

三、循环处理:重复的艺术

循环让你可以一遍又一遍地执行相同的代码块,直到满足某个条件。

3.1 for 循环:遍历列表

语法一:遍历值列表

for 变量 in 项1 项2 项3 ... 项N
do
# 对每一项执行的操作
done

示例:处理多个文件

#!/bin/bash

# 循环处理当前目录下所有 .txt 文件
for file in *.txt
do
echo "正在处理文件: $file"
# 可以在这里执行 cp, mv, grep 等操作
done

# 遍历一个固定的列表
for fruit in "Apple" "Banana" "Orange"
do
echo "我喜欢吃 $fruit"
done

语法二:C 语言风格的 for 循环

for (( 初始值; 条件; 步进 ))
do
# 循环体
done

示例:数字迭代

#!/bin/bash

# 从 1 循环到 5
for (( i=1; i<=5; i++ ))
do
echo "当前数字: $i"
done

3.2 while 循环:只要条件为真,就继续循环

while 循环会在循环开始时检查条件,只要条件为真(返回状态码为 0),就继续执行循环体。

while [ 条件 ]
do
# 循环体命令
done

示例:计数器

#!/bin/bash

count=1
while [ $count -le 5 ] # 只要 count 小于等于 5
do
echo "计数: $count"
count=$((count + 1)) # 另一种算术运算写法:((count++))
done

经典用法:读取文件每一行

#!/bin/bash

echo "文件内容如下:"
while IFS= read -r line # IFS= 防止行首尾空格被忽略,-r 防止反斜杠转义
do
echo ">>> $line"
done < /path/to/file.txt # 输入重定向,将文件内容喂给 while 循环

3.3 until 循环:直到条件为真,才停止循环

untilwhile 相反。它一直循环,直到条件变为真为止。

until [ 条件 ]
do
# 循环体命令
done

示例:等待某个进程启动

#!/bin/bash

echo "等待数据库服务启动..."
until pg_isready -q -h localhost # 直到这个检查数据库的命令成功
do
echo "数据库还未就绪,睡眠 2 秒..."
sleep 2
done
echo "数据库已启动!"

四、循环控制:break 与 continue

这两个命令用于在循环内部进行更精细的控制。

4.1 break:立即跳出循环

break 用于无条件地终止并跳出当前循环,执行 done 后面的命令。

示例:在数组中查找元素,找到后立即退出

#!/bin/bash

names=("Alice" "Bob" "Charlie" "David")
target="Charlie"

for name in "${names[@]}"
do
echo "正在检查: $name"
if [ "$name" = "$target" ]; then
echo "找到了!$target 在列表中。"
break # 找到后就跳出循环,不再检查后面的元素
fi
done

跳出多层循环break n 可以跳出 n 层循环。

for ((i=1; i<=3; i++))
do
for ((j=1; j<=3; j++))
do
if [ some_condition ]; then
break 2 # 直接跳出内外两层循环
fi
done
done

4.2 continue:跳过本次循环

continue 用于跳过当前循环中剩余的语句,直接开始下一次循环。

示例:只处理奇数

#!/bin/bash

for number in {1..10}
do
if (( number % 2 == 0 )); then # 如果是偶数
continue # 跳过后续命令,开始下一个循环(处理下一个数字)
fi
echo "这是一个奇数: $number"
done

五、实战综合案例:自动化备份脚本

让我们用一个融合了多种控制语句的实战案例来结束本章。

#!/bin/bash
# 综合案例:增量备份脚本

# 1. 定义变量
BACKUP_SRC="/home/user/important_data" # 要备份的源目录
BACKUP_DST="/backup" # 备份目标目录
TIMESTAMP=$(date +%Y%m%d_%H%M%S) # 时间戳
BACKUP_FILE="backup_$TIMESTAMP.tar.gz" # 备份文件名
LOG_FILE="$BACKUP_DST/backup.log" # 日志文件

# 2. 条件判断:检查源目录是否存在
if [ ! -d "$BACKUP_SRC" ]; then
echo "[ERROR] $(date): 备份源目录 $BACKUP_SRC 不存在!" | tee -a "$LOG_FILE"
exit 1
fi

# 3. 条件判断:检查目标目录是否存在,不存在则创建
if [ ! -d "$BACKUP_DST" ]; then
echo "备份目录不存在,正在创建..."
mkdir -p "$BACKUP_DST"
fi

# 4. 开始备份流程
echo "[INFO] $(date): 开始备份 $BACKUP_SRC$BACKUP_DST/$BACKUP_FILE..." | tee -a "$LOG_FILE"

# 5. 使用 tar 命令创建压缩包,并判断是否成功
if tar -czf "$BACKUP_DST/$BACKUP_FILE" -C "$(dirname "$BACKUP_SRC")" "$(basename "$BACKUP_SRC")" 2>/dev/null; then
echo "[SUCCESS] $(date): 备份成功完成!" | tee -a "$LOG_FILE"
else
echo "[ERROR] $(date): 备份过程中出现错误!" | tee -a "$LOG_FILE"
exit 1
fi

# 6. 循环:清理超过 30 天的旧备份
echo "[INFO] $(date): 开始清理旧备份..." | tee -a "$LOG_FILE"
find "$BACKUP_DST" -name "backup_*.tar.gz" -mtime +30 | while read -r old_backup; do
echo "正在删除旧备份: $old_backup"
rm -f "$old_backup"
echo "已删除: $old_backup" >> "$LOG_FILE"
done

echo "[INFO] $(date): 所有操作已完成。" | tee -a "$LOG_FILE"

总结与最佳实践

Shell 的控制语句赋予了脚本灵魂,使其变得强大而灵活。

  1. 选择合适的工具

    • if/case 用于分支选择
    • for 适合已知循环次数遍历集合
    • while/until 适合未知循环次数,取决于某个条件
  2. 重要提醒

    • 空格是语法:在 [ ][[ ]](( )) 中,括号内的空格是必须的,否则会报错。
    • 变量加引号:在 [ ] 中引用变量时,最好用双引号括起来,如 [ -f "$file" ],以防止变量为空或含空格导致语法错误。
    • 使用 [[ ]]:在现代 Bash 脚本中,优先使用 [[ ]] 进行条件测试,它比 [ ] 更强大、更安全(支持 &&, ||, =~ 正则匹配等)。
    • 算术比较用 (( )):进行数字计算和比较时,使用 (( )) 更直观,如 if (( count > 10 )); then ...

通过熟练组合运用条件判断、分支选择和各种循环,你将能够解决 Shell 环境中绝大多数自动化任务和系统管理问题。