Shell 脚本条件判断与流程控制:if for while case 详解

1. 引言:为什么流程控制是 Shell 脚本的"方向盘"

凌晨两点,运维小李被报警电话叫醒:一台生产服务器的磁盘使用率已经飙到 92%。他熟练地登录服务器,敲下几行命令清理了过期日志,然后打开一个月前写好的 Shell 监控脚本,确认脚本里"磁盘超过阈值就自动清理并告警"的逻辑已经生效,才放心地继续睡觉。这个脚本的核心,正是我们今天要讲的主角------条件判断与流程控制

很多初学者以为 Shell 脚本就是"把命令一行行排在一起从上往下执行"。这种理解在任务简单时没问题,但一旦遇到真实场景------"日志目录超过 2GB 就压缩归档""批量重启 50 台服务器并在失败时重试""根据用户输入执行不同操作"------你很快会发现,没有 if、for、while、case 这些"方向盘",脚本就像一辆只会直行的车,稍微拐个弯就冲出跑道。

流程控制语句让脚本拥有了"思考"和"循环"的能力:它可以根据条件选择走哪条路,也可以不厌其烦地重复同一件事成千上万次。掌握了它们,你的脚本才真正从"命令清单"升级为"自动化程序"。

本文将从最基础的条件测试讲起,逐步深入 if、for、while、case 四大核心语句,配合大量可直接运行的示例和真实工作场景,帮你彻底打通 Shell 流程控制这一关。

2. 基础铺垫:test 命令与条件测试

在学 if 之前,必须先搞懂"条件"是怎么表达和判断的。Shell 中判断一个条件是否成立,靠的是 test 命令

2.1 test 的两种写法

test 是一个专门用来做判断的命令,判断结果通过退出状态码 (exit code)返回:条件成立返回 0,条件不成立返回 1。这一点和大多数编程语言"真为 1、假为 0"的直觉相反,请务必牢记:Shell 中 0 代表成功/真,非 0 代表失败/假

bash 复制代码
test 10 -gt 5     # 判断 10 是否大于 5
echo $?           # 输出 0,表示条件成立

test 有个更常用的等价写法------方括号 [ ]。注意,[ 其实是一个命令(没错,它和 test 是同一个命令的两个名字),所以左方括号后面、右方括号前面都必须有空格

bash 复制代码
[ 10 -gt 5 ]
echo $?           # 输出 0

此外,bash 还提供了增强版的 [[ ]],它是 bash 的关键字(而非外部命令),功能更强、更安全,后文会详细对比。

2.2 常用的条件测试

条件测试主要分为三类,下面用表格汇总最常用的操作符。

数值比较

操作符 含义 示例
-eq 等于(equal) [ "$a" -eq "$b" ]
-ne 不等于(not equal) [ "$a" -ne "$b" ]
-gt 大于(greater than) [ "$a" -gt "$b" ]
-ge 大于等于(greater or equal) [ "$a" -ge "$b" ]
-lt 小于(less than) [ "$a" -lt "$b" ]
-le 小于等于(less or equal) [ "$a" -le "$b" ]

字符串比较

操作符 含义 示例
=== 字符串相等 [ "$a" = "$b" ]
!= 字符串不相等 [ "$a" != "$b" ]
-z 字符串长度为 0(为空) [ -z "$a" ]
-n 字符串长度不为 0(非空) [ -n "$a" ]

文件测试

操作符 含义
-e 文件存在
-f 是普通文件
-d 是目录
-r / -w / -x 可读 / 可写 / 可执行
-s 文件存在且非空
-nt / -ot 比另一个文件新 / 旧

2.3 条件组合

多个条件可以用逻辑运算组合起来:

bash 复制代码
[ "$score" -ge 60 ] && [ "$score" -lt 100 ]   # 与:两个都要成立
[ "$role" = "admin" ] || [ "$role" = "root" ] # 或:满足其一即可
[ ! -f /tmp/lock ]                            # 非:取反
  • &&(与):左边成立才执行右边
  • ||(或):左边失败才执行右边
  • !(非):取反

注意事项&&|| 具有"短路"特性。A && B 中如果 A 失败,B 根本不会执行;A || B 中如果 A 成功,B 也不会执行。这个特性常被用来写简洁的"一行式"判断,比如 [ -f config ] && echo "存在" || echo "不存在"

2.4 [ ][[ ]] 的区别

刚开始写脚本时,很多人的报错都来自 [ ][[ ]] 用混。简单说:

  • [ ] 是 POSIX 标准的 test 命令,兼容性最好,但语法要求苛刻(空格、引号一个都不能少)。
  • [[ ]] 是 bash 扩展,功能更强:支持 &&/|| 直接写在括号内、支持 =~ 正则匹配、通配符匹配,且不需要对变量加双引号也不会因变量为空而报错
bash 复制代码
#!/bin/bash
name=""

# 用 [ ] 判断空字符串,忘记加引号会报错
[ -z $name ]        # 出错:一元操作符预期错误

# 用 [[ ]] 则安全得多
[[ -z $name ]] && echo "name 为空"    # 正常输出

建议 :写 bash 脚本时优先使用 [[ ]] 做条件判断,既安全又强大;只有在追求跨平台 POSIX 兼容(如 dash、某些精简版 Linux)时才用 [ ]

3. if 条件判断:让脚本学会"见机行事"

if 是流程控制的基石,它让脚本根据条件的真假选择不同的执行路径。你可以把它想象成十字路口的红绿灯:条件成立走绿灯方向,不成立走红灯方向。

3.1 语法格式

if 有三种基本形态。

单分支

bash 复制代码
if 条件; then
    命令
fi

双分支

bash 复制代码
if 条件; then
    命令1
else
    命令2
fi

多分支

bash 复制代码
if 条件1; then
    命令1
elif 条件2; then
    命令2
else
    命令3
fi

注意几点:

  • ifthen 写在同一行时,中间要用分号 ; 隔开;也可以把 then 单独放下一行,此时分号可以省略。
  • 每个 if 必须以 fi 结尾(fi 就是 if 倒过来写,帮助记忆)。
  • elif 是 else if 的简写,一个 if 中可以有多个 elif。

3.2 示例一:判断成绩等级

bash 复制代码
#!/bin/bash
read -p "请输入你的分数(0-100):" score

if [[ $score -ge 90 ]]; then
    echo "优秀!继续保持!"
elif [[ $score -ge 80 ]]; then
    echo "良好,还可以更进一步。"
elif [[ $score -ge 60 ]]; then
    echo "及格,需要加油哦。"
else
    echo "不及格,建议重新学习一遍基础知识。"
fi

示例输出

复制代码
请输入你的分数(0-100):85
良好,还可以更进一步。

3.3 示例二:判断文件类型(结合文件测试)

bash 复制代码
#!/bin/bash
path="/tmp/demo"

if [[ -d $path ]]; then
    echo "$path 是一个目录"
elif [[ -f $path ]]; then
    echo "$path 是一个普通文件"
elif [[ -e $path ]]; then
    echo "$path 存在,但不是普通文件或目录(可能是链接或设备)"
else
    echo "$path 不存在"
fi

3.4 示例三:真实场景------磁盘空间告警

这个例子直接对应引言中的运维场景,建议动手跑一跑:

bash 复制代码
#!/bin/bash
# 获取根分区使用率(去掉百分号)
usage=$(df -h / | awk 'NR==2{print $5}' | tr -d '%')

if [[ $usage -ge 90 ]]; then
    echo "【严重】磁盘使用率 ${usage}%,请立即清理!"
elif [[ $usage -ge 80 ]]; then
    echo "【警告】磁盘使用率 ${usage}%,建议关注。"
else
    echo "【正常】磁盘使用率 ${usage}%。"
fi

示例输出

复制代码
【正常】磁盘使用率 42%。

注意事项

  1. if 后面的条件本质上是一条命令,判断的是它的退出状态码,所以 if 命令; then 也是合法的(比如 if grep -q "error" logfile; then ...)。
  2. 比较数值时一定要用 -gt-lt 这类专用操作符,不要用 ><------它们在 [ ] 里会被当作重定向符号,导致意想不到的错误。

4. for 循环:把重复的事情交给"复读机"

运维工作中有大量"批量操作"场景:批量创建用户、批量拷贝文件、遍历目录下的所有日志。for 循环就是为这类场景而生------它把一件事重复做 N 次,每次换一个不同的"元素"。

4.1 语法格式

for 有两种常用写法。

写法一:遍历列表(最常用):

bash 复制代码
for 变量 in 列表; do
    命令
done

列表可以是直接写出的值(用空格分隔)、一个变量的展开结果、命令的输出(如 $(ls))、通配符展开的文件名(如 *.log)等。

写法二:C 语言风格

bash 复制代码
for ((初始值; 条件; 步进)); do
    命令
done

这种写法适合需要精确控制循环次数、有明确起始值和步长的场景,比如从 1 累加到 100。

4.2 示例一:遍历一个列表

bash 复制代码
#!/bin/bash
for fruit in apple banana cherry; do
    echo "我喜欢吃 $fruit"
done

示例输出

复制代码
我喜欢吃 apple
我喜欢吃 banana
我喜欢吃 cherry

4.3 示例二:批量处理文件(真实场景)

假设 /var/log/app 目录下有很多 .log 文件,要把它们统一压缩归档:

bash 复制代码
#!/bin/bash
for file in /var/log/app/*.log; do
    echo "正在压缩 $file ..."
    gzip "$file"
done
echo "全部处理完成!"

注意事项 :如果目录下没有任何 .log 文件,*.log 不会被展开,循环体仍会执行一次,此时 $file 的值就是字面的 /var/log/app/*.log。为避免这种尴尬,可以在循环开头判断文件是否存在:

bash 复制代码
for file in /var/log/app/*.log; do
    [[ -e $file ]] || continue   # 文件不存在就跳过本次循环
    gzip "$file"
done

4.4 示例三:C 风格 for 计算 1 到 100 的和

bash 复制代码
#!/bin/bash
sum=0
for ((i=1; i<=100; i++)); do
    sum=$((sum + i))
done
echo "1 到 100 的和是:$sum"

示例输出

复制代码
1 到 100 的和是:5050

4.5 示例四:批量创建用户

bash 复制代码
#!/bin/bash
for user in zhangsan lisi wangwu; do
    if id "$user" &>/dev/null; then
        echo "用户 $user 已存在,跳过"
    else
        useradd "$user" && echo "用户 $user 创建成功"
    fi
done

这个例子展示了 for 与 if 的组合使用:循环负责"遍历用户列表",if 负责"判断用户是否已存在"。流程控制语句可以任意嵌套,这是写出实用脚本的关键。

5. while 循环:不达目的不罢休的"哨兵"

for 循环适合"已知要执行多少次"的场景,而 while 循环适合"不知道几次,直到条件不满足为止"的场景。它像一个永不懈怠的哨兵,只要条件成立,就一直站在原地执行任务;条件不成立,它立刻收工。

5.1 语法格式

bash 复制代码
while 条件; do
    命令
done

只要"条件"成立(退出状态码为 0),循环体就会一直执行。因此循环体内部必须有能改变条件的语句,否则就会陷入死循环。

5.2 示例一:计数器循环

bash 复制代码
#!/bin/bash
count=1
while [[ $count -le 5 ]]; do
    echo "第 $count 次执行"
    count=$((count + 1))
done
echo "循环结束,共执行了 $((count - 1)) 次"

示例输出

复制代码
第 1 次执行
第 2 次执行
第 3 次执行
第 4 次执行
第 5 次执行
循环结束,共执行了 5 次

注意 count=$((count + 1)) 这行------它是让循环"刹车"的关键。如果漏掉这行,脚本就会无限打印"第 1 次执行",只能按 Ctrl+C 强制中断。

5.3 示例二:逐行读取文件(真实场景)

while 配合 read 命令逐行读取文件,是处理配置、日志、名单的经典手法:

bash 复制代码
#!/bin/bash
# 假设 ip_list.txt 每行一个 IP 地址
while read -r ip; do
    echo "正在探测 $ip ..."
    ping -c 1 -W 1 "$ip" &>/dev/null \
        && echo "$ip 在线" \
        || echo "$ip 不通"
done < ip_list.txt

< ip_list.txt 把文件内容重定向给 while 循环,read 每次读一行赋给变量 ip,读到文件末尾时 read 返回非 0,循环自动结束。

注意事项 :这里必须用 done < file 而不是 cat file | while read,虽然后者也能工作,但它会在子 shell 中运行循环体,导致循环内部的变量修改无法传递到循环外。初学者踩这个坑最多,务必记住。

5.4 示例三:无限循环与服务守护

while 也常用于写"常驻脚本",比如每隔几秒检查一次服务状态:

bash 复制代码
#!/bin/bash
while true; do
    if ! pgrep -x "nginx" &>/dev/null; then
        echo "$(date):nginx 已挂,尝试拉起..."
        systemctl start nginx
    fi
    sleep 10
done

while true 的写法表示"永远循环",全靠 sleep 10 控制节奏、靠外部 Ctrl+C 才能退出。这种结构常用于守护进程、实时监控脚本。

6. case 多分支选择:菜单脚本的"总开关"

当需要根据一个变量的多个取值来执行不同操作时,if-elif-elif-... 会越写越长、越写越难读。case 语句就是为"多选一"场景量身定做的------它像老式电话总机的插线板,把不同的"来电"(变量取值)转接到对应的"分机"(命令块)。

6.1 语法格式

bash 复制代码
case 变量 in
模式1)
    命令1
    ;;
模式2)
    命令2
    ;;
模式3|模式4)
    命令3
    ;;
*)
    默认命令
    ;;
esac

语法要点:

  • 每个分支以 模式) 开头,以 ;; 结尾(双分号表示分支结束)。
  • 模式3|模式4 中的 | 表示"或",匹配其一即可。
  • *) 是通配符,匹配所有其它情况,相当于 if 的 else,通常放在最后。
  • case 以 esac 结尾(case 倒过来写)。

6.2 示例一:服务管理脚本(真实场景)

这是 case 最经典的应用------写一个服务启停脚本:

bash 复制代码
#!/bin/bash
read -p "请输入操作(start/stop/restart/status):" action

case $action in
start)
    echo "正在启动服务..."
    systemctl start nginx
    ;;
stop)
    echo "正在停止服务..."
    systemctl stop nginx
    ;;
restart)
    echo "正在重启服务..."
    systemctl restart nginx
    ;;
status)
    echo "当前服务状态:"
    systemctl status nginx
    ;;
*)
    echo "用法:$0 {start|stop|restart|status}"
    ;;
esac

示例输出

复制代码
请输入操作(start/stop/restart/status):restart
正在重启服务...

6.3 示例二:匹配模式(通配符的力量)

case 的模式匹配支持 Shell 通配符,这使它比 if 的等值判断强大得多:

bash 复制代码
#!/bin/bash
read -p "请输入文件名:" filename

case $filename in
*.txt)
    echo "这是一个文本文件"
    ;;
*.sh)
    echo "这是一个 Shell 脚本"
    ;;
*.tar.gz|*.zip)
    echo "这是一个压缩包"
    ;;
*)
    echo "无法识别的文件类型:$filename"
    ;;
esac

注意事项

  1. 每个分支结尾的 ;; 不能省略,否则会"穿透"到下一个分支继续执行(Shell 不像 C 语言那样默认贯穿)。
  2. case 对变量的取值不加引号 也不会做单词拆分,但如果变量里可能包含空格或特殊字符,建议写成 case "$变量" in 更稳妥。

7. 循环控制:break、continue 与 exit

循环有了,还需要"方向盘"和"急刹车"。breakcontinueexit 就是控制循环流程的三件套。

命令 作用 类比
break 立即跳出整个循环 直接下车结束行程
continue 跳过本次循环,进入下一次 这一站不下车,继续开
exit 退出整个脚本 终止整趟旅程
bash 复制代码
#!/bin/bash
for i in {1..10}; do
    if [[ $i -eq 3 ]]; then
        continue      # 跳过 3 不打印
    fi
    if [[ $i -eq 8 ]]; then
        break         # 到 8 直接跳出循环
    fi
    echo "当前数字:$i"
done
echo "循环提前结束"

示例输出

复制代码
当前数字:1
当前数字:2
当前数字:4
当前数字:5
当前数字:6
当前数字:7
循环提前结束

breakcontinue 还可以带数字参数,表示跳出/跳过几层 嵌套循环,例如 break 2 表示跳出两层循环。多层嵌套时这个技巧非常实用。

8. 知识扩展:Shell 脚本是如何执行起来的

理解了四大语句的用法后,不妨往底层看一眼:当我们敲下 ./script.sh 时,系统到底做了什么?理解这个过程,能帮你解释很多"玄学"报错。

  1. 解释执行,而非编译 。Shell 脚本是解释型语言,由 /bin/bash(或 shebang 指定的解释器)逐行读取、逐行解释执行。所以脚本里哪怕第 100 行有语法错误,前面 99 行也已经执行完了------这和编译型语言"一处报错、全部罢工"完全不同。
  2. 进程与子进程 。脚本运行时,bash 会 fork 出一个子进程来执行它。脚本里的每条外部命令又会在子进程下再 fork 出"孙进程"。而 ifforwhilecase 这些是 bash 的内建语法,由 bash 自己解释,不产生新进程,这就是它们比外部命令快的原因之一。
  3. test 命令的身世 。前面提到 [ 是 test 命令的别名。早期 Unix 系统中并没有 [,人们只能写 test 10 -gt 5。后来开发者觉得这种写法不够直观,就创建了一个 [ 的硬链接指向 test,于是 [ 10 -gt 5 ] 诞生了。那个看似多余的右括号 ] 其实只是个"装饰参数",test 并不真正使用它,但为了可读性和语法完整性,它被保留至今。而 [[ ]] 则是 bash 后来引入的关键字,它不再是外部命令,因此不需要 fork 子进程、支持更多高级特性。

原理探究 :可以用 type 命令验证这一点------type [ 会显示 [ 是一个 shell 内建命令,type test 类似,而 which [ 能找到 /usr/bin/[ 这个实际存在的可执行文件。内建命令优先于外部命令被执行,这是 Shell 效率优化的一个细节。

9. 常见错误与避坑指南

这一节汇集了初学者最常踩的坑,提前看过能省下大量排错时间。

9.1 空格之坑

[]、操作符、操作数之间都必须有空格,少了任何一个都会报错:

bash 复制代码
[10 -gt 5]      # 错:缺少空格
[ 10 -gt 5]     # 对
[$a -eq $b]     # 错
[ $a -eq $b ]   # 对

判断字符串相等时尤其容易出错:

bash 复制代码
[ $a="hello" ]   # 错!这实际上是把 "hello" 赋值给 $a 的写法
[ "$a" = "hello" ]   # 对:= 两边要有空格

9.2 赋值与比较混淆

赋值用一个 等号且不加空格,比较用两个等号且加空格:

bash 复制代码
a=5          # 赋值
[[ $a == 5 ]]   # 比较(在 [[ ]] 中 = 和 == 都可以)

9.3 变量未加引号

变量为空或含空格时,不加引号会引发各种诡异错误:

bash 复制代码
name=""
[ $name = "zhangsan" ]   # 展开后变成 [ = "zhangsan" ],语法错误
[ "$name" = "zhangsan" ] # 正确

[ ] 中养成"变量一律加双引号"的习惯,能避免 90% 的此类问题;使用 [[ ]] 则天然规避。

9.4 死循环

while 忘记写改变条件的语句,或 for 的列表恒为真,都会导致 CPU 占满:

bash 复制代码
while true; do
    echo "停不下来了..."
    # 没有 sleep 也没有 break,危险!
done

写无限循环时,务必在循环体内加 sleep 或确保有 break 出口。

10. 最佳实践:写出专业可维护的脚本

  1. 文件头规范 :第一行写 shebang(#!/bin/bash),并加上脚本用途、作者、日期等注释。
  2. 变量命名见名知意disk_usagex 好,log_dird 好。
  3. 优先 [[ ]] :写 bash 代码时默认用 [[ ]] 做条件判断,安全又现代。
  4. 复杂条件拆解:单个 if 条件超过两三个逻辑运算时,拆成多个 if 或用变量承载中间结果,可读性远胜于"一行梭哈"。
  5. 循环加防护 :批量删除、覆盖等危险操作前,先 echo 打印要操作的命令,确认无误再真正执行;或加 read -p "确认继续?" 二次确认。
  6. 使用 set -euo pipefail:在脚本开头加上这行,可以在命令失败时自动退出、未定义变量时报错、管道中任意一环失败都返回失败,极大提升脚本健壮性。
bash 复制代码
#!/bin/bash
set -euo pipefail

11. 总结与练习

至此,我们已经完整掌握了 Shell 条件判断与流程控制的核心知识:用 test/[[ ]] 表达条件,用 if 做选择,用 for/while 做循环,用 case 做多分支,用 break/continue/exit 控制流程走向。它们就像搭积木一样可以自由组合,几乎能实现你能想到的任何自动化逻辑。

核心回顾

  • 条件判断的本质是命令的退出状态码,0 代表真,非 0 代表假。
  • if 适合"二选一"或"多选一"的决策,elif 越多越建议换成 case
  • for 适合已知次数的遍历,while 适合未知次数的持续执行。
  • case 擅长根据变量取值分派任务,模式匹配支持通配符。
  • break/continue/exit 是循环的"方向盘、变速器和总闸"。

练习题(建议动手完成后再看提示):

练习 1:写一个脚本,读取用户输入的年份,判断是否为闰年(能被 4 整除但不能被 100 整除,或能被 400 整除)。

提示:使用 [[ $year % 4 -eq 0 ]] 做取模判断,注意组合条件。

练习 2:用 for 循环打印九九乘法表。

提示:双层 for,内层循环变量从 1 到外层变量;echo -n 不换行,内层结束后 echo 换行。

练习 3:用 while 循环实现一个简单的"猜数字"游戏:程序生成随机数(1-100),用户猜,大了提示"大了",小了提示"小了",猜中退出。

提示:用 $RANDOM 生成随机数,\[ `$RANDOM % 100 + 1` ] 得到 1-100 的数;循环条件为"没猜中"。

练习 4 :写一个 case 版的文件类型识别器,能识别 .txt.md.jpg/.png.mp4 以及其它类型。

提示:结合 *.jpg|*.png) 的"或"模式,其余用 *) 兜底。

练习 5 :扫描指定目录下所有 .log 文件,统计每个文件的单词数,超过 10000 个单词的文件打印"大文件"标记。

提示:for 遍历 + wc -w 提取单词数 + if 判断,注意 $(...) 命令替换的用法。

学习 Shell 没有捷径,最好的方法就是把每个示例敲一遍、改一遍、错一遍、再修一遍。当你在凌晨从容地依赖自己写的脚本守护生产环境时,会感谢今天每一个"踩坑"的瞬间。祝你写得开心,运维不加班!

相关推荐
captain3761 小时前
网络原理(3)-TCP核心机制连接管理▲▲▲
java·服务器·网络·ide·网络协议·tcp/ip·java-ee
mounter6251 小时前
统一安全域:KVM Planes 如何驾驭硬件特权级
linux·安全·linux kernel·kernel
君生我老1 小时前
Linux动静态库
linux·服务器
troy1281 小时前
Python 基础语法(一):变量、数据类型与运算符
java·服务器·python
IT摆渡者2 小时前
基于 Docker 部署 Nextcloud 私有云盘(外挂 Windows 共享)项目实践
运维·docker·容器
宵时待雨2 小时前
linux笔记归纳19:网络层协议IP
linux·网络·笔记·网络协议·tcp/ip
程序员清风2 小时前
记忆系统设计:让 Agent 拥有短期记忆与长期记忆
java·服务器·网络
做运维的阿瑞2 小时前
Linux下DNS服务器搭建
linux·运维·服务器
团子股股东峥哥2 小时前
day37-RHEL-管理基本存储
linux·运维·服务器