第 9 章 · 02 条件判断:test、[ 与 [[ ]] 本节摘要:脚本光会顺序执行还不够,它得学会「看情况办事」。本节讲 Shell 的条件判断三件套: 、 、 。前两者其实是同一个东西的两种写法( 就是 命令,只是末尾要求一个 ),而 是 Bash 内建关键字,比 更强更安全。你会学到三类判断:字符串比较( / )、数值比较( / / )、文件判定( / / / )。最关键的认知升级是: 是一条命令,不是语法括号——这意味着 后面和 前面必须有空格,变量不加引号就可能因为「空值导致参数消失」而报错。理解了 是命令、 是关键字,你就能解释它们所有的行为差异。 内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
本节摘要:脚本光会顺序执行还不够,它得学会「看情况办事」。本节讲 Shell 的条件判断三件套:
test、[、[[ ]]。前两者其实是同一个东西的两种写法([就是test命令,只是末尾要求一个]),而[[ ]]是 Bash 内建关键字,比[更强更安全。你会学到三类判断:字符串比较(=/!=)、数值比较(-eq/-lt/-gt)、文件判定(-f/-d/-e/-r)。最关键的认知升级是:[ ]是一条命令,不是语法括号——这意味着[后面和]前面必须有空格,变量不加引号就可能因为「空值导致参数消失」而报错。理解了[是命令、[[ ]]是关键字,你就能解释它们所有的行为差异。
内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
阅读完本节,你应当能够:
test、[、[[ ]] 三者的关系:test 与 [ 是同一命令的两种写法,[[ ]] 是 Bash 内建关键字。=、!=、-z、-n)判断相等、不等、空、非空。-eq、-ne、-lt、-gt、-le、-ge)做数值判断,并说清它和字符串比较的区别。-f、-d、-e、-r、-w、-s)检查文件类型与权限。&&、||、! 组合多个条件,区分 [ ] 中要用 -a/-o 而 [[ ]] 中直接用 &&/||。if/while/until 里正确嵌套条件,写出能跑的分支逻辑。test 和 [ 是同一个东西很多人以为 [ 是「语法括号」,其实不是。[ 是一条货真价实的命令,它的真名是 test:
test -f file.txt # 用 test 命令判断 [ -f file.txt ] # 用 [ 命令判断,效果完全一样
[ 就是 test 命令的一个别名,唯一的区别是:用 [ 时必须在末尾加一个 ] 作为最后一个参数(纯粹是为了视觉上配对,test 不需要)。两者返回同样的退出码:判断为真返回 0,为假返回非 0。
关键概念:在 Shell 里,「真」是退出码 0,「假」是非 0。这和 C/Python 的「0 是假」正好相反。原因在第一章讲过:退出码 0 表示「成功」,命令成功了就被当成「条件成立」。
既然 [ 是命令,它就遵守命令的规则:参数之间必须用空格分开。所以 [ -f file.txt ] 里 [ 后、] 前都必须有空格,否则 Shell 不认。
[[ ]] 是 Bash 内建关键字,更强更安全Bash 引入了 [[ ]],它不是命令,而是 Shell 解析阶段的「关键字」。这个区别带来三个优势:
# 优势 1:变量为空也不会报错([ ] 会) x="" [ $x = "" ] # 错!展开后变成 [ = "" ],参数个数不对,报错 [ "$x" = "" ] # 对!加引号才能用 [[ $x = "" ]] # 对![[ ]] 自动处理,变量不加引号也安全 # 优势 2:直接用 && || 组合([ ] 要用 -a -o) [ -f a -a -f b ] # 传统写法,-a 表示逻辑与 [[ -f a && -f b ]] # 现代写法,&& 更直观 # 优势 3:支持模式匹配(通配符) [[ $name == a* ]] # name 以 a 开头则为真(== 在 [[ ]] 里是模式匹配)
💡 技巧:写 Bash 脚本时,条件判断一律用
[[ ]]。它比[安全(变量为空不报错)、强大(支持模式匹配和正则)、可读性好(直接&&/||)。只有写必须兼容 POSIXsh的脚本时才退回[。
if 的本质:判断退出码if 命令; then ... fi
if 后面跟的是一条命令,Shell 根据它的退出码决定走哪个分支。所以 if [ -f file ] 其实是 if 后面跟着 [ -f file ] 这条命令。同理:
if grep -q error log; then echo "有错"; fi # grep 找到匹配返回 0,if 就走 then if cd /tmp; then echo "进来了"; fi # 任何命令都能放进 if
理解了「if 判断的是命令的退出码」,你就掌握了 Shell 条件判断的全部精髓。
a="hello" b="world" [[ $a == "hello" ]] # 相等 [[ $a != $b ]] # 不等 [[ -z "$a" ]] # 长度为 0(空字符串) [[ -n "$a" ]] # 长度非 0(非空)
⚠️ 注意:字符串比较用
=还是==?在[[ ]]里两者都行,且==是模式匹配;在[ ]里 POSIX 只保证=,==是扩展。为了通用,[ ]里用=,[[ ]]里用==。
数值比较不能用 >、<、==(那是字符串比较,而且 < > 在 Shell 里是重定向符!),要用专门的运算符:
| 运算符 | 含义 | 示例 |
|---|---|---|
-eq |
等于 (equal) | [[ $n -eq 5 ]] |
-ne |
不等 (not equal) | [[ $n -ne 5 ]] |
-lt |
小于 (less than) | [[ $n -lt 10 ]] |
-gt |
大于 (greater than) | [[ $n -gt 10 ]] |
-le |
小于等于 | [[ $n -le 10 ]] |
-ge |
大于等于 | [[ $n -ge 10 ]] |
count=5 if [[ $count -lt 10 ]]; then echo "还不到 10" fi # C 风格数值比较([[ ]] 独有,更直观) if (( count < 10 )); then echo "还不到 10" fi
💡 技巧:
(( ))是算术上下文(注意不是$(()),那是展开),里面可以直接用<>==,更像主流语言。if (( n > 10 )); then比if [[ $n -gt 10 ]]; then更易读。纯数值比较优先用(( ))。
这是 Shell 脚本最常用的判断——文件存不存在、是什么类型、能不能读写。
| 运算符 | 含义 |
|---|---|
-e |
存在(任何类型) |
-f |
存在且是普通文件 |
-d |
存在且是目录 |
-r |
存在且可读 |
-w |
存在且可写 |
-x |
存在且可执行 |
-s |
存在且大小非 0 |
-L |
存在且是符号链接 |
if [[ -f /etc/hosts ]]; then echo "配置文件存在" fi if [[ ! -d /var/log/myapp ]]; then mkdir -p /var/log/myapp fi # 经典:脚本必须有配置文件才能跑 config=/etc/myapp.conf if [[ ! -f $config ]]; then echo "找不到配置文件 $config" >&2 exit 1 fi
&&、||、!# [[ ]] 里直接用 && || ! if [[ -f file && -r file ]]; then # 是文件且可读 cat file fi if [[ ! -e lockfile ]]; then # 锁文件不存在 echo $$ > lockfile fi # 逻辑运算的短路特性,单行写法(不用 if) [[ -f file ]] && echo "存在" # 条件真才执行后面 [[ -d dir ]] || mkdir dir # 条件假才执行后面(目录不存在就建)
cmd1 && cmd2 || cmd3 是常见的「三目」写法:cmd1 成功则跑 cmd2,否则跑 cmd3。但要小心 cmd2 失败会误触发 cmd3,复杂逻辑还是用 if。
[[ ]] 独有)# 模式匹配(用 == 或 !=,右边不加引号才生效) [[ $file == *.txt ]] && echo "文本文件" [[ $name == a* ]] && echo "以 a 开头" # 正则匹配(用 =~) [[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "像 IP" [[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+$ ]] && echo "像邮箱"
⚠️ 注意:模式匹配时,右边的模式不能加引号(
[[ $x == "a*" ]]会变成字面匹配字符a*)。只有想匹配字面字符串时才加引号。
[ 后面和 ] 前面忘加空格if [-f file]; then # 错![-f 被当成一个命令名 if [ -f file ]; then # 对![ 后 ] 前都有空格
记住:[ 是命令,命令和参数之间必须有空格。
[ ] 里变量不加引号,空值导致报错x="" [ $x = "y" ] # 错!展开成 [ = y ],参数缺失,报错 "unary operator expected" [ "$x" = "y" ] # 对!展开成 [ "" = "y" ],正常 [[ $x = "y" ]] # 最对![[ ]] 不需要引号也安全
这是 [ ] 最大的坑。用 [ ] 时所有变量必须加双引号,或者干脆改用 [[ ]]。
< >[ 5 > 3 ] # 错!> 是重定向,会创建一个名为 3 的文件 [ 5 -gt 3 ] # 对!用 -gt [[ 5 -gt 3 ]] # 对 (( 5 > 3 )) # 对!算术上下文里 > 才是大于
= 和比较 = 搞混if [ $a = $b ] # 这是字符串比较,不是赋值 if (( a == b )) # 数值比较用 == a=5 # 这才是赋值(if 外面)
= 在 []/[[]] 里是比较,在别处(不带空格)是赋值。语境不同,含义不同。
if 后面忘了 then,或 then 没换行if [[ -f file ]] then # 错!then 前要有 ; 或换行 if [[ -f file ]]; then # 对 if [[ -f file ]] then # 对 ... fi
test 和 [ 是同一个命令的两种写法,[ 就是 test 加一个配对的 ],是外部命令,遵守命令规则(参数间必须空格)。[[ ]] 是 Bash 内建关键字,比 [ 更强:变量为空不报错、支持 &&/||、支持模式匹配与正则。写 Bash 脚本优先用 [[ ]]。if 判断的是命令的退出码,0 为真、非 0 为假。if grep -q error log 这种写法很常见。=/!=/-z/-n)、数值(-eq/-lt/-gt 等)、文件(-f/-d/-e/-r 等)。(( )) 更直观:(( n > 10 )) 比 [[ $n -gt 10 ]] 更像主流语言。[ ] 内侧忘空格、[ ] 里变量不加引号致空值报错、数值比较误用 </>(被当重定向)。[[ $x == a* ]] 匹配以 a 开头,[[ $x == "a*" ]] 则匹配字面 a*。条件判断让脚本有了「分支」。下一节我们给它加上「重复」——for、while、until 三种循环。for 用来遍历列表或文件,while 用来「只要条件成立就一直做」,until 是 while 的反面。循环配上 break/continue,脚本能处理的任务复杂度就上了一个台阶。