在 Linux Shell 脚本编程中,条件分支、参数校验与流程控制几乎无处不在。然而,许多初学者乃至有一定经验的工程师,经常在条件判断处写出隐蔽的语法 Bug——例如变量未加双引号导致报错 unary operator expected、括号前后遗漏空格导致解析失败,或是混淆了逻辑与运算符号 -a 与 &&。
更重要的是,Shell 中的“布尔真假”与绝大多数高级编程语言(如 C/Python/Java)有着截然相反的底层定义。本文将从底层退出状态码讲起,系统拆解 Shell 中的布尔运算、条件测试与命令列表短路求值实战。
一、方案速查与核心测试项矩阵
在编写条件测试时,务必熟悉三大核心测试类型与对应的常用操作符:
1. 三类条件测试操作符速查表
| 测试类型 | 操作符 | 功能描述 | 典型代码示例 | 注意事项 |
|---|---|---|---|---|
| 数值比较 | -eq |
等于 (Equal) | [ $a -eq 5 ] |
仅支持整型数值比较,不可用 == |
-ne |
不等于 (Not Equal) | [ $a -ne 0 ] |
替代 ! [ $a -eq 0 ] |
|
-gt / -ge |
大于 / 大于等于 | [ $count -ge 10 ] |
大于等于 Greater / Equal | |
-lt / -le |
小于 / 小于等于 | [ $i -lt 100 ] |
小于 Less / Equal | |
| 字符串 | -z |
判断字符串长度为 0 (为空) | [ -z "$str" ] |
变量必须加双引号 |
-n |
判断字符串非空 | [ -n "$str" ] |
等同于 [ "$str" != "" ] |
|
= / == |
判断两字符串相等 | [ "$a" = "$b" ] |
POSIX 规范推荐单个 = |
|
!= |
判断两字符串不相等 | [ "$a" != "$b" ] |
区分大小写 | |
| 文件系统 | -f |
是否为普通文件且存在 | [ -f "/etc/hosts" ] |
排除目录与套接字 |
-d |
是否为目录且存在 | [ -d "/var/log" ] |
专用于目录校验 | |
-s |
文件存在且大小大于 0 (非空) | [ -s "$log_file" ] |
适合判断是否有错误输出 | |
-x |
文件存在且具备可执行权限 | [ -x "/usr/bin/python3" ] |
适合脚本执行前自检 | |
-e |
文件/目录是否存在 (Exists) | [ -e "$path" ] |
任意文件类型均可 |
2. 逻辑连接符对比:-a / -o vs && / ||
| 逻辑类型 | test 内部参数 |
外部逻辑连接符 | 现代 [[ ... ]] 语法 |
推荐级别 |
|---|---|---|---|---|
| 逻辑与 (AND) | test -a / [ ... -a ... ] |
[ ... ] && [ ... ] |
[[ ... && ... ]] |
优先用 && 或 [[ ... && ... ]] |
| 逻辑或 (OR) | test -o / [ ... -o ... ] |
[ ... ] \|\| [ ... ] |
[[ ... \|\| ... ]] |
优先用 \|\| 或 [[ ... \|\| ... ]] |
| 逻辑非 (NOT) | test ! / [ ! ... ] |
! [ ... ] |
[[ ! ... ]] |
两者皆可 |
[!WARNING] 废弃警告:POSIX 官方标准明确将
test内部的-a和-o列为过时(Obsolescent)语法。在复杂表达式中,由于操作符优先级解析的歧义极易引发诡异 Bug,强烈建议使用[ cond1 ] && [ cond2 ]或直接采用 Bash 高级测试[[ cond1 && cond2 ]]!
二、Shell 布尔逻辑的本质认知
1. 退出状态码 $? 与逻辑真假(颠覆认知的“0 为真”)
在 C/Java/Python 等高级语言中,非 0(通常为 1)表示真,0 表示假; 但在 Linux Shell 中,规则恰恰相反!
- 退出码
$? == 0:表示进程或命令执行成功(Success),在逻辑分支中判定为 真 (True); - 退出码
$? != 0(1~255):表示执行遇到错误或失败(Error),在逻辑分支中判定为 假 (False);
# 验证经典 if 分支与退出码关系
if true; then
echo "true 执行成功,判定为真" # 执行此处
fi
if false; then
echo "false 执行失败"
else
echo "false 退出码非 0,判定为假" # 执行此处
fi
2. 内置命令 true 与 false 的底层探秘
true 和 false 并不是关键字,而是真正的 Shell 内置命令或独立程序:
$ type true false
true is a shell builtin
false is a shell builtin
$ true
$ echo "true 的退出码是: $?" # 输出 0!表示成功/真
$ false
$ echo "false 的退出码是: $?" # 输出 1!表示失败/假
查看帮助文档更一目了然:
true: Return a successful result. (返回成功状态码 0)
false: Return an unsuccessful result. (返回失败状态码 1)
三、条件测试与三类核心判断实战
1. 数值属性比较
数值比较专门用来判定两个整数的大小关系(注意:不能用于浮点数):
a=5
b=10
# 1. 判定相等 (-eq) 与 不等 (-ne)
if [ $a -eq 5 ]; then
echo "a 等于 5"
fi
if [ $a -ne $b ]; then
echo "a 与 b 不相等"
fi
# 2. 判定大小区间 (-gt, -lt, -ge, -le)
if [ $a -lt $b ]; then
echo "a 小于 b"
fi
2. 字符串属性与防御性引用
字符串测试用于判断文本内容是否为空、是否相等等。在 Shell 中处理字符串时,防御性加双引号是必须养成的肌肉记忆!
name="admin"
empty_str=""
# 1. 判断是否为空 (-z: Zero length)
if [ -z "$empty_str" ]; then
echo "字符串为空"
fi
# 2. 判断是否非空 (-n: Non-zero length)
if [ -n "$name" ]; then
echo "用户名为: $name"
fi
# 3. 字符串精准相等比较
if [ "$name" = "admin" ]; then
echo "身份确认为管理员"
fi
3. 文件系统属性测试
在部署与自动化运维脚本中,操作文件或目录之前必须先行探测其属性:
target_conf="/etc/nginx/nginx.conf"
backup_dir="/backup/data"
# 1. 判断是否为普通文件 (-f)
[ -f "$target_conf" ] && echo "配置文件存在且为普通文件"
# 2. 判断是否为目录 (-d)
[ -d "$backup_dir" ] || mkdir -p "$backup_dir"
# 3. 判断文件是否有实际内容 (-s: Size > 0)
# 特别适合检测命令错误日志是否为空
error_log="/tmp/app_err.log"
if [ -s "$error_log" ]; then
echo "检测到异常错误输出,正在发送告警通知..."
fi
# 4. 判断文件是否具备执行权限 (-x)
if [ -x "./deploy.sh" ]; then
./deploy.sh
else
chmod +x ./deploy.sh && ./deploy.sh
fi
四、多重逻辑组合与经典避坑指南
1. 经典陷阱一:方括号两端空格遗漏
方括号 [ 在 Linux 中本质上是一个命令(/usr/bin/[ 是 test 命令的别名软链接)。因此,它的每个参数之间必须严格用空格分隔!
# 错误示范:缺少空格会报错
# if [$a -eq 5]; then ... fi -> -bash: [5: command not found
# if [ "$str" = "test"]; then -> -bash: [: missing `]'
# 正确示范:[ 之后与 ] 之前必须保持空格
if [ "$str" = "test" ]; then
echo "匹配成功"
fi
2. 经典陷阱二:变量未加双引号引发 unary operator expected
当未加双引号的变量本身为空("")或包含空格时,Shell 在展开变量后会导致参数数量错乱:
str=""
# 危险写法:如果 str 为空,[ $str = "test" ] 会直接被展开为:[ = "test" ]
# 导致左侧参数直接消失,抛出一元操作符缺失异常!
# if [ $str = "test" ]; then ... fi -> -bash: [: =: unary operator expected
# 健壮写法:永远用双引号包裹
if [ "$str" = "test" ]; then
echo "安全验证"
fi
3. 现代演进:拥抱 [[ ... ]] 复合条件判断
在编写针对现代 Bash(Bash 3.0+)的脚本时,强烈推荐使用 [[ ... ]] 双中括号:
a=5
b=10
str="hello world"
# 优势 1: 内部无需担心空格变量展开崩解,即使不加双引号也安全
if [[ $str == "hello world" ]]; then
echo "完全匹配"
fi
# 优势 2: 原生支持 && 与 ||,支持通配符与正则匹配 (=~)
if [[ $a -gt 0 && $b -lt 20 ]]; then
echo "数值在预期范围内"
fi
# 优势 3: 正则匹配校验纯数字
ip="192.168.1.1"
if [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "IP 格式合法"
fi
五、命令列表与短路求值实战
由于逻辑运算遵循短路求值(Short-circuit evaluation)机制,我们可以用极其优雅精炼的 && 与 || 命令列表,替代冗长厚重的 if-then-else 结构。
1. 短路执行黄金法则
command1 && command2: 只有当command1返回 0(成功) 时,才会执行command2;command1 || command2: 只有当command1返回 非 0(失败) 时,才会执行command2。
# 连通性测试实战:连通才输出状态
ping -c 1 -W 1 127.0.0.1 >/dev/null 2>&1 && echo "网络畅通!"
# 目录不存在则自动创建 (守护式创建)
[ -d "/opt/app" ] || mkdir -p "/opt/app"
2. 一行流生产级脚本参数校验
在脚本开头进行入参检查与前置断言时,短路语法极其简洁有力:
#!/bin/bash
# demo_check.sh - 生产级入参短路自检模板
TARGET_FILE=$1
EXPECT_PORT=$2
# 断言参数至少有 2 个,否则输出用法并直接以状态码 1 退出
[ $# -ge 2 ] || { echo "错误: 参数不足!用法: $0 <file> <port>"; exit 1; }
# 断言目标文件必须存在
[ -f "$TARGET_FILE" ] || { echo "错误: 目标文件 $TARGET_FILE 不存在!"; exit 2; }
# 断言端口必须为纯整数
[[ "$EXPECT_PORT" =~ ^[0-9]+$ ]] || { echo "错误: 端口必须为整型数值!"; exit 3; }
echo "参数自检全部通过,开始执行业务逻辑..."
3. Shell 模拟三元运算符的正确姿势
我们常用 cond && cmd1 || cmd2 来模拟高级语言中的三元表达式 cond ? cmd1 : cmd2:
score=85
[ $score -ge 60 ] && result="及格" || result="不及格"
echo "考核结果: $result" # 输出: 及格
[!WARNING] 三元模式的隐蔽陷阱: 如果
cmd1本身执行失败(返回非 0 状态码),短路或会误穿透并执行cmd2! 举个反例:[ 1 -eq 1 ] && ( echo "真"; exit 1 ) || echo "居然执行了假!",由于前面的子 shell 退出码是 1,||会误认为整体失败进而触发右侧命令。 避坑准则:只有当中间的cmd1必然返回状态码 0 时(如纯变量赋值),才可使用三元写法;否则一律使用标准的if ... else ... fi!
六、总结与工程选型建议
编写可靠、健壮的 Shell 逻辑判断,请遵循以下工程准则:
- 语法选型首选:
- 现代 Bash 脚本中优先采用
[[ ... ]]进行复合条件判断,原生支持&&、||、通配符与正则匹配; - 若需保证极致的 POSIX
/bin/sh可移植性,使用单括号[ ... ],并严格给每个字符串变量加双引号。 - 符号严禁混淆:
- 数值比较用
-eq,-ne,-lt,-gt,绝不要在[ ]中使用==或<; - 字符串比较用
=和!=; - 杜绝使用废弃的
-a/-o内部参数,改用&&/||连接独立测试项。 - 推崇极简短路:
- 善用
[ -d "$dir" ] || mkdir -p "$dir"等短路模式消灭不必要的嵌套if代码块,提升脚本可读性与工业质感。