Linux Shell if 条件判断完全指南:从基础语法到高级特性
概述
写 Shell 脚本的时候,你是不是经常需要让脚本看情况办事?比如:
- 判断文件在不在,在就删,不在就给个提示;
- 检查命令跑没跑成功,成功就继续,失败就退出;
- 比较一下数值大小,超过某个阈值就报警。
if 条件判断语句 就是 Shell 脚本的“决策核心”——它能根据“命令执行结果”或者“表达式真假”来决定要不要执行某段代码,让脚本从“一条道走到黑”变成“该走哪条走哪条”。
说白了,if 语句最核心的价值就是:让脚本长出“如果…就…”的判断能力,这也是脚本能自动化、变聪明的根基。
一、基础 if 语法:从 if-then 到 if-then-elif-else
Shell 里 if 语句的核心逻辑是“看命令的退出状态码”——命令退出状态码是 0(成功),就执行后面的代码;不是 0,就跳过。先从最基础的语法讲起。
1. 最基础:if-then 语句(单分支)
条件满足就跑一段代码,不满足就跳过,这是最常用的单分支判断。
标准语法(推荐)
if 待判断命令
then
# 命令执行成功(退出状态码 0)时,执行这里的代码
代码块
fi # 标志 if 语句结束(必须写,否则脚本报错)
紧凑语法(一行写法)
若代码块简单,可将 then 与 “待判断命令” 写在同一行,用分号 ; 分隔(分号表示命令结束):
if 待判断命令; then
代码块
fi
关键原理:退出状态码
Shell 的 if 看的是“if 后面那条命令的退出状态码”,不是像 Python、C 那样看“等式是真还是假”:
- 退出状态码 0:命令执行成功 → 执行 then 后面的代码;
- 退出状态码 1~255:命令执行失败 → 跳过 then 后面的代码。
示例 1:判断命令是否执行成功
#!/bin/bash
# 示例:判断 pwd 命令是否执行成功(pwd 是系统命令,正常情况下一定成功)
if pwd # 执行 pwd 命令,退出状态码为 0
then
echo "✅ pwd 命令执行成功,当前路径已打印"
fi
执行效果:
/home/roger # pwd 命令的输出
✅ pwd 命令执行成功,当前路径已打印 # then 后的代码被执行
示例 2:判断命令执行失败的情况
#!/bin/bash
# 示例:判断不存在的命令(IamNotACommand)是否执行成功
if IamNotACommand # 命令不存在,退出状态码非 0
then
echo "✅ 命令执行成功" # 这行代码会被跳过
fi
echo "🔚 脚本执行结束(无论 if 结果如何,都会执行这行)"
执行效果:
./test.sh: line 3: IamNotACommand: command not found # 命令不存在的错误提示
🔚 脚本执行结束(无论 if 结果如何,都会执行这行) # 跳过 then 代码,执行后续内容
2. 双分支:if-then-else 语句(满足 / 不满足都处理)
条件满足跑一段代码,不满足跑另一段,正反两种情况都覆盖到。
语法格式
if 待判断命令
then
# 命令成功(状态码 0)→ 执行这里
代码块1
else
# 命令失败(状态码非 0)→ 执行这里
代码块2
fi
示例:判断文件是否存在
用 test -f 文件名 命令判断文件是否为普通文件(-f 是 test 命令的选项),若存在则删除,不存在则提示:
#!/bin/bash
file_path="test.txt"
# 判断 test.txt 是否为普通文件
if test -f "$file_path"
then
echo "🗑️ $file_path 存在,正在删除..."
rm "$file_path" # 删除文件
else
echo "ℹ️ $file_path 不存在,无需删除"
fi
执行效果(文件存在时):
🗑️ test.txt 存在,正在删除...
执行效果(文件不存在时):
ℹ️ test.txt 不存在,无需删除
提示:test -f “$file_path” 可简化为 [ -f “$file_path” ](中括号与内容之间必须有空格),是 Shell 中的常用写法。
3. 多分支:if-then-elif-else 语句(多个条件判断)
要判一堆递进的条件,用 elif 一路往下接(比套好几层 if 清爽),最后用 else 兜底,接住所有没匹配上的情况。
语法格式
if 命令1 # 第一个条件
then
代码块1 # 命令1 成功时执行
elif 命令2 # 第一个条件失败时,判断第二个条件
then
代码块2 # 命令2 成功时执行
elif 命令3 # 第二个条件失败时,判断第三个条件
then
代码块3 # 命令3 成功时执行
else
代码块4 # 所有条件都失败时执行
fi
示例:根据分数判断等级
#!/bin/bash
read -p "请输入你的分数(0-100):" score
# 多分支判断分数等级
if [ $score -ge 90 ]; then # -ge 表示“大于等于”(数值比较)
echo "🏆 你的等级是:优秀"
elif [ $score -ge 80 ]; then
echo "🥈 你的等级是:良好"
elif [ $score -ge 60 ]; then
echo "✅ 你的等级是:及格"
else
echo "❌ 你的等级是:不及格"
fi
执行效果(输入 85 时):
请输入你的分数(0-100):85
🥈 你的等级是:良好
执行效果(输入 55 时):
请输入你的分数(0-100):55
❌ 你的等级是:不及格
二、if 语句的高级特性:单括号、双括号、双方括号
基础的 if 靠 test 命令或者 [ ] 来判断,功能比较有限。Shell 另外给了三种高级写法,分别解决“在子 shell 里跑命令”“复杂数学计算”“高级字符串匹配”这几件事。
1. 单括号 (command):子 shell 中执行命令
用 (命令) 把命令包起来时,Shell 会先开一个“子 shell”(独立于当前脚本的 shell 进程),在子 shell 里执行这些命令,再拿它的退出状态码来判断。
它的核心作用就一个:把命令的影响挡在子 shell 里,比如子 shell 里改了变量,不会污染到当前脚本。
语法格式
if (待在子 shell 中执行的命令)
then
命令成功时执行的代码
fi
关键概念:子 shell 与当前 shell 的区别
可以用 $BASH_SUBSHELL 变量看当前处在第几层 shell:
- 当前 shell 是 0 层;
- 子 shell 是 1 层(如果子 shell 里再套子 shell,就是 2 层,以此类推)。
示例:子 shell 执行命令的隔离效果
#!/bin/bash
echo "当前 shell 层级:$BASH_SUBSHELL" # 输出 0(当前 shell)
# 在子 shell 中执行命令,修改变量并查看层级
if (
echo "子 shell 层级:$BASH_SUBSHELL" # 输出 1(子 shell)
sub_var="我是子 shell 中的变量" # 在子 shell 中定义变量
echo "子 shell 中:sub_var = $sub_var"
)
then
echo "✅ 子 shell 命令执行成功"
fi
# 查看当前脚本中是否能访问子 shell 的变量
echo "当前 shell 中:sub_var = $sub_var" # 输出空(变量未被继承)
执行效果:
当前 shell 层级:0
子 shell 层级:1
子 shell 中:sub_var = 我是子 shell 中的变量
✅ 子 shell 命令执行成功
当前 shell 中:sub_var = # 子 shell 变量未传递到当前 shell
适用场景:跑那些会改变环境变量、工作目录的命令(比如 cd /tmp)时,用这个把它圈在子 shell 里,免得影响到脚本后面的逻辑。
2. 双括号 ((expression)):高级数学计算
基础的 [ ] 只能做简单的加减乘除(+ - * /),双括号 (( )) 能搞更复杂的数学运算(比如幂运算、自增自减),而且写法更接近别的编程语言。
语法格式
if (( 数学表达式 )) # 表达式中变量可加 $,也可不加(推荐不加,更简洁)
then
表达式为真时执行的代码
fi
支持的数学运算符(常用)
| 运算符 | 含义 | 示例 |
|---|---|---|
| ** | 幂运算(次方) | ((a = 2 ** 3)) → a=8 |
| ++ / — | 自增 / 自减(前缀 / 后缀) | ((a++)) → a 先使用后加 1 |
| += / -= | 累加 / 累减 | ((a += 5)) → a = a+5 |
| > / < | 大于 / 小于(无需转义) | ((a > 10)) → 判断 a 是否大于 10 |
| >= / <= | 大于等于 / 小于等于 | ((a >= 5)) |
| && / || | 逻辑与 / 逻辑或 | ((a > 5 && b < 10)) |
示例 1:幂运算判断
#!/bin/bash
val=10
# 判断 10 的平方是否大于 90(双括号支持 ** 幂运算)
if (( val ** 2 > 90 )) # val 无需加 $,直接使用变量名
then
square=$((val ** 2)) # 计算平方值
echo "✅ $val 的平方是 $square,大于 90"
fi
执行效果:
✅ 10 的平方是 100,大于 90
示例 2:逻辑组合判断
#!/bin/bash
a=15
b=8
# 判断 a 大于 10 且 b 小于 10(双括号支持 && 逻辑与)
if (( a > 10 && b < 10 ))
then
echo "✅ 条件满足:a=$a >10,且 b=$b <10"
fi
执行效果:
✅ 条件满足:a=15 >10,且 b=8 <10
3. 双方括号 [[expression]]:高级字符串处理
基础的 [ ] 只支持简单的字符串比较(等于、不等于这种),双方括号 [[]] 多了“模式匹配”的能力(用通配符或者正则去匹配字符串),而且逻辑运算符(&& ||)用起来更直观。
语法格式
if [[ 字符串表达式 ]] # 支持通配符、正则(部分 Shell 版本支持)
then
表达式为真时执行的代码
fi
核心特性:模式匹配(通配符)
支持 *(匹配任意字符)、?(匹配单个字符)、[ ](匹配指定范围的字符)这些通配符,而且不用转义。
示例 1:判断字符串是否以指定前缀开头
#!/bin/bash
read -p "请输入你的邮箱:" email
# 判断邮箱是否以 @gmail.com 结尾(* 匹配 @gmail.com 前的任意字符)
if [[ $email == *@gmail.com ]] # == 表示字符串比较,* 是通配符
then
echo "✅ 你使用的是 Gmail 邮箱"
else
echo "❌ 你使用的不是 Gmail 邮箱"
fi
执行效果(输入 test@gmail.com 时):
请输入你的邮箱:test@gmail.com
✅ 你使用的是 Gmail 邮箱
执行效果(输入 test@qq.com 时):
请输入你的邮箱:test@qq.com
❌ 你使用的不是 Gmail 邮箱
示例 2:判断字符串是否匹配指定格式
#!/bin/bash
read -p "请输入一个数字(1-3 位):" num
# 判断是否为 1-3 位数字([0-9] 匹配单个数字,{1,3} 匹配 1-3 次)
if [[ $num =~ ^[0-9]{1,3}$ ]] # =~ 表示正则匹配(bash 3.0+ 支持)
then
echo "✅ 输入正确:$num 是 1-3 位数字"
else
echo "❌ 输入错误:请输入 1-3 位数字"
fi
执行效果(输入 123 时):
请输入一个数字(1-3 位):123
✅ 输入正确:123 是 1-3 位数字
执行效果(输入 1234 时):
请输入一个数字(1-3 位):1234
❌ 输入错误:请输入 1-3 位数字
注意:双方括号是 bash 特有的写法,脚本要是用 sh 跑(比如 sh test.sh)可能会报错,记得把脚本开头写成 #!/bin/bash。
四、常见问题与避坑指南
用 if 语句时,新手经常因为语法细节或者逻辑理解偏差把脚本写报错,下面把高频问题和解法列一下:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| [ ] 中报错 “syntax error”(语法错误) | 1. 中括号与内容之间无空格(如 [1==2]); 2. 变量未加双引号,含空格时被拆分为多个参数 | 1. 确保中括号前后有空格(如 [ 1 == 2 ]); 2. 变量用双引号包裹(如 [ “$var” == “test” ])。 |
| 双括号中数学比较报错 | 1. 使用了字符串比较运算符(如 == 用于数值); 2. 变量含非数字字符 | 1. 数值比较用 > < >= 等(如 ((a > 10))); 2. 确保变量是纯数字(可先用 [[$var =~ ^[0-9]+$]] 校验)。 |
| 双方括号模式匹配失效 | 1. 用 sh 执行脚本(sh 不支持双方括号); 2. 通配符加了双引号(被当作普通字符) | 1. 用 bash 执行脚本(如 bash test.sh),或脚本开头写 #!/bin/bash; 2. 通配符不加双引号(如 [[$str == test]])。 |
| if 后直接写等式(如 if $a == $b)报错 | 混淆 Shell 与其他语言的语法,Shell if 后必须跟 “命令”,而非 “等式” | 1. 数值等式用 ((a == b)); 2. 字符串等式用 [[$a == $b]] 或 [ “$a” == “$b” ]。 |
| 判断文件时,含空格的路径报错 | 路径变量未加双引号,被拆分为多个参数 | 路径变量必须用双引号包裹(如 [ -f”$file_path” ]),避免空格导致路径被拆分。 |
| 逻辑运算符 &&||在[ ]中报错 | [ ]不支持&&||,需用 -a(逻辑与)、-o(逻辑或),或嵌套 if | 1. [ ]中用-a-o(如 [ $a -gt 10 -a $b -lt 20 ]); 2. 推荐用 [[]]或(( ))直接用&&||(如 [[ $a -gt 10 && $b -lt 20]]) |
四、实战案例:3 个高频运维场景
把 if 语句的基础语法和高级特性用起来,解决几个运维里常见的实际需求,顺便巩固一下。
案例 1:批量备份日志文件(基础 if-then-else)
需求:把 /var/log 目录下的 .log 文件过一遍,超过 100MB 的压缩备份一份(原文件留着),没超的就跳过。
#!/bin/bash
# 批量备份 /var/log 下超过 100MB 的 .log 文件
log_dir="/var/log"
backup_suffix="_$(date '+%Y%m%d').gz" # 备份文件后缀(带日期)
# 遍历目录下的 .log 文件
for log_file in "$log_dir"/*.log; do
# 跳过不存在的文件(避免通配符未匹配时的空值)
[ -f "$log_file" ] || continue
# 1. 获取文件大小(单位:MB,用 du -m 计算,取第一列数值)
file_size=$(du -m "$log_file" | awk '{print $1}')
# 2. 判断文件大小是否超过 100MB
if [ $file_size -gt 100 ]; then
# 压缩备份(gzip -c 输出到标准输出,重定向到备份文件)
gzip -c "$log_file" > "${log_file}${backup_suffix}"
echo "✅ 备份成功:${log_file}(${file_size}MB)→ ${log_file}${backup_suffix}"
else
echo "ℹ️ 跳过:${log_file}(${file_size}MB ≤ 100MB)"
fi
done
执行效果(需 sudo 权限,因 /var/log 属主为 root):
sudo ./backup_logs.sh
# 输出:
ℹ️ 跳过:/var/log/auth.log(8MB ≤ 100MB)
✅ 备份成功:/var/log/syslog.log(120MB)→ /var/log/syslog.log_20240602.gz
ℹ️ 跳过:/var/log/kern.log(15MB ≤ 100MB)
核心知识点:du -m 计算文件大小、[ $file_size -gt 100 ] 数值比较、for 循环遍历文件。
案例 2:检查服务运行状态(if + 命令退出状态码)
需求:检查 nginx 服务是否运行,若未运行则自动启动,若启动失败则输出报警信息。
#!/bin/bash
# 检查并自动恢复 nginx 服务
service_name="nginx"
# 1. 检查服务是否运行(systemctl is-active 命令,成功则退出状态码 0)
if systemctl is-active --quiet "$service_name"; then
echo "✅ $service_name 服务正在运行"
else
echo "❌ $service_name 服务未运行,尝试启动..."
# 2. 尝试启动服务
if systemctl start "$service_name"; then
echo "✅ $service_name 服务启动成功"
else
# 3. 启动失败,输出报警(可扩展为邮件/短信报警)
echo "⚠️ 报警:$service_name 服务启动失败,请手动检查!"
exit 1 # 退出脚本,状态码 1 表示错误
fi
fi
执行效果(服务未运行时):
❌ nginx 服务未运行,尝试启动...
✅ nginx 服务启动成功
执行效果(服务启动失败时):
❌ nginx 服务未运行,尝试启动...
Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
⚠️ 报警:nginx 服务启动失败,请手动检查!
核心知识点:利用命令退出状态码判断服务状态、嵌套 if 处理 “启动成功 / 失败” 分支。
案例 3:验证用户输入的 IP 地址(if + 正则匹配)
需求:让用户输入 IP 地址,用正则匹配验证格式是否合法(IPv4),合法则输出 “有效”,非法则提示重新输入。
#!/bin/bash
# 验证用户输入的 IPv4 地址格式
read -p "请输入 IPv4 地址:" ip_addr
# IPv4 正则表达式:匹配 xxx.xxx.xxx.xxx,其中每个 xxx 是 0-255 的数字
ipv4_regex='^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
# 用双方括号的正则匹配功能验证
if [[ $ip_addr =~ $ipv4_regex ]]; then
echo "✅ 输入有效:$ip_addr 是合法的 IPv4 地址"
else
echo "❌ 输入无效:$ip_addr 不是合法的 IPv4 地址(格式应为 xxx.xxx.xxx.xxx,每个段 0-255)"
exit 1
fi
执行效果(输入 192.168.1.1 时):
请输入 IPv4 地址:192.168.1.1
✅ 输入有效:192.168.1.1 是合法的 IPv4 地址
执行效果(输入 256.0.0.1 时):
请输入 IPv4 地址:256.0.0.1
❌ 输入无效:256.0.0.1 不是合法的 IPv4 地址(格式应为 xxx.xxx.xxx.xxx,每个段 0-255)
核心知识点:双方括号的 =~ 正则匹配、IPv4 正则表达式编写。
五、总结:if 语句的最佳实践
-
语法怎么选:
- 比数值、做计算:优先用 (( )),能搞复杂运算,写起来也清爽;
- 比字符串、做模式匹配:优先用 [[]],通配符、正则都支持,逻辑运算符也直观;
- 要是得兼容 sh:那就用 [ ],记得用 -a -o 代替 && ||。
-
变量怎么处理:
- 字符串 / 路径变量:一定要用双引号包起来(不然碰到空格、特殊字符会被拆开或者解析出错);
- 数值变量:可以省略 $(比如 ((val > 10))),但自己写的时候保持一致就好。
-
判断怎么写更省事:
- 别套太多层 if:多分支用 elif,简单的判断直接用 && || 化简(比如 [ -f “$file” ] && rm “$file”,就是“文件在就删”);
- 异常情况提前处理:比如遍历文件时,先用 [ -f “$file” ] || continue 把无效文件跳掉,里层判断就少了。
-
可读性:
- 复杂条件加注释:比如正则、多个条件组合,顺手注明在判什么;
- 代码块缩进:then / else 后面的代码用 2 或 4 个空格缩进,看着清楚(Shell 不强制,但建议养成习惯)。
if 语句就是 Shell 脚本的“逻辑骨架”。把基础语法和高级特性搞熟,再结合循环、函数这些,就能写出逻辑清楚、稳当的自动化运维脚本。建议从简单例子练起,慢慢往复杂场景过渡,踩过几次坑之后,写脚本的效率自然就上来了。