写shell脚本绕不开条件判断,说它是脚本的逻辑中枢毫不夸张。一个if写错,轻则逻辑错乱,重则把线上文件清空,这种事在运维圈里太常见了。前阵子帮一个转岗的同事review脚本,他写的判断条件是if [ $var = "success" ],结果变量为空时直接报错,排查了半天才发现是少了双引号。这篇文章就从最基础的退出码讲起,把if、case、test、[]和[[]]这些条件语句的底细彻底捋一遍,把我这几年在真实环境里踩过的坑也一并说清楚,适合刚入门的开发、运维,也给写过一些脚本但总在各种边界条件上翻车的同学做个系统梳理。
1. 条件语句的核心机制:先搞懂退出码
很多初学者学条件判断时,总觉得if是个魔法关键字。实际上在shell的世界里,一切判断都建立在退出码(exit code,也叫返回码)这个最底层的概念上。你用if包住的任何命令、任何表达式,最终都会被shell转化为一个进程退出码,然后由if根据这个码是0还是非0来决定走哪个分支。
1.1 退出码是Unix世界里最原始的“真与假”
你打开终端,随手敲一条命令,比如ls /tmp,执行完毕之后shell会记录它返回一个数字,这就是退出码。数字0代表成功,非0代表失败,这是POSIX标准约定俗成的规矩。注意这和我们平时写C语言、Java里的逻辑刚好相反,那些语言里通常true是1、false是0,但shell的退出码里0才是“一切正常”。
所以if condition; then ...; fi的本质,不是判断condition这个表达式本身是什么,而是执行condition这条东西,看它结束后的退出码是否为0。理解了这一点,很多看着奇怪的写法就都能解释了。比如你可以直接写:
bash复制if grep -q "error" /var/log/app.log; then
echo "日志里发现error关键字"
fi
这不是把grep的“结果内容”当作判断条件,而是把grep -q这条命令的退出码当作判断条件。grep找到了匹配就返回0,找不到就返回1,if通过这个退出码来决定执行哪个分支。同理,你还能写:
bash复制if cd /some/dir; then
echo "目录切换成功"
fi
cd成功会返回0,失败会返回非0,加个if就是“如果目录切换成了,就干某事”。
想随时查看上一条命令的退出码,可以直接用echo $?。这个变量是shell内置的特殊变量,保存着最近一次前台执行命令的退出码。排错的时候这个变量极其好使,尤其是脚本里某个环节莫名没生效的时候,先打印一下$?往往就能定位问题。
1.2 不要拿脚本里最后的退出码当摆设
写脚本时有个很容易被忽略的习惯:脚本本身也是一个“程序”,它结束时也应该有一个退出码。如果你在脚本最后什么都不写,那么整个脚本的退出码就是最后一条命令的退出码。这有时会掩盖脚本里的真实失败。
我在实际工作里见过一个非常典型的场景:脚本里跑了一个很长的数据同步任务,中间某一步失败了,但因为最后一步是一条echo输出“流程结束”之类的信息,echo永远返回0,于是整份脚本的退出码就是0。上层的调度平台(比如Jenkins、Airflow)看到退出码0,就认为脚本执行成功,结果数据其实是残缺的。这就是条件判断意识不强的后果。
正确的做法是,关键的失败路径上要主动返回非0。比如:
bash复制if ! check_data; then
echo "数据校验失败,退出"
exit 1
fi
再配合脚本开头加上set -e,让脚本在某条命令失败时直接中断退出。但要小心,set -e不是万能的,管道命令的退出码默认只看最后一个命令,像cmd1 | cmd2这种,如果cmd1失败了但cmd2成功了,整个管道返回0,set -e也救不了你。这种时候可以用set -o pipefail,让管道中任何一条命令失败都会让整个管道返回非0。运维老手通常会在脚本头部写set -euo pipefail这四条组合,其中-u是变量未定义直接报错,能避免很多因变量名拼写错误导致的玄学bug。
1.3 快速自查:命令执行成功后怎么验证退出码
有个调试技巧我觉得值得分享。想确认某个命令在特定场景下到底返回什么退出码,可以这样:
bash复制false
echo $? # 输出1
true
echo $? # 输出0
ls /nonexistent_file
echo $? # 通常是2
true和false是shell里的两个特殊命令,什么也不做,只分别返回0和1。测试条件语句时用它们做占位符再方便不过。比如你临时想试某个if/else分支结构对不对,又不想真的执行一个耗时命令,就可以写if false; then ... else ... fi,测试完再替换成真实条件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. if语句的完整结构与多分支逻辑
说完退出码,再来看if本身。Shell的if结构没有圆括号包起来的C语言风格,是靠保留字if、then、elif、else、fi拼起来的。fi就是if的反写,表示“if语句结束了”,跟case结束用esac(case的反写)一个套路。这套词法看着别致,但用熟了就会觉得很顺手。
2.1 基础写法与格式上的硬性要求
最标准的if写法是:
bash复制if <判断内容>; then
分支语句...
fi
注意判断内容和then之间必须用分号或换行隔开。写成两行也行:
bash复制if <判断内容>
then
分支语句...
fi
很多新手第一次写shell,容易把then和判断写在同一行却忘了分号,结果直接语法报错。这个问题我见过不下十次。
判断内容可以是任意一条命令,常用的是test命令,以及它的别名写法[ ... ]。比如:
bash复制if [ "$name" = "hello" ]; then
echo "名字符合预期"
fi
这里[其实是一个程序的名字,系统里通常有个/usr/bin/[二进制文件,它与test命令等价。所以[ "$name" = "hello" ]整个是在调用一条命令,命令的退出码决定了if的分支走向。注意[的左右两侧必须有空格,]的前面也必须有空格,否则shell会把它当成变量展开的一部分或普通字符。[ "$name" = "hello" ]如果写成[$name = "hello"],基本必然报错或判断异常。
2.2 elif是else if的语法糖,但也可能掩盖逻辑问题
一个if可以有多个elif分支:
bash复制if [ "$score" -ge 90 ]; then
echo "优秀"
elif [ "$score" -ge 80 ]; then
echo "良好"
elif [ "$score" -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
这个结构执行时,shell会从上往下找第一个退出码为0的条件,找到就进入对应的分支,后续的elif、else全部跳过。所以分支条件的顺序会影响结果。比如上面这段,如果第二个条件写的是[ "$score" -ge 60 ],那80分也会命中“及格”,因为80同样>=60。写多分支时一定要把“更严格的条件”放前面,这是一个很重要的经验。
还有一个容易翻车的点:变量没有初始化时,多分支的else可能会兜底成功。举个例子:
bash复制if [ "$mode" = "fast" ]; then
echo "快速模式"
elif [ "$mode" = "slow" ]; then
echo "慢速模式"
else
echo "未知模式"
fi
如果$mode压根没定义,那么“未知模式”就会被打印。这在某些场景下是业务上兜底,但在严格环境下可能掩盖配置项缺失的问题。更好的做法是提前判断变量是否存在,或者干脆用set -u让未定义变量直接报错。
2.3 短写法:一行if与逻辑运算符的替代
Shell允许把if写在一行里。比如:
bash复制if [ -f /tmp/a.txt ]; then echo "文件存在"; fi
这种简洁写法在交互式shell里很实用,但在脚本里如果分支逻辑稍复杂,还是建议拆成多行,可读性会好很多。真正在脚本里大量使用的是逻辑运算符替代写法:
bash复制[ -f /tmp/a.txt ] && echo "文件存在" || echo "文件不存在"
&&表示前一条命令退出码为0才执行后者,||表示前一条命令退出码非0才执行后者。这个写法本质上就是if/else的简写。但要小心:如果&&后面的命令也返回了非0,那么||后面的命令也会执行,这会造成错觉。比如:
bash复制[ -f /tmp/a.txt ] && cp /tmp/a.txt /tmp/b.txt || echo "文件不存在"
如果[ -f /tmp/a.txt ]成功但cp因为权限等原因失败了,||后面的echo也会执行,你看到的输出就成了“文件不存在”,完全误导人。所以我个人的习惯是,涉及多条命令的场景老老实实写if/else,只有单条命令时才用&&和||短写法。
3. 条件表达式全解析:test、[]与[[]]的差异
条件语句的核心部分其实是里面的表达式。Shell条件表达式主要涉及字符串判断、数字大小比较、文件属性判断、逻辑组合这几类。很多人写脚本时拿到别人的代码,一会儿看到[一会儿看到[[,搞不清到底哪个对,这里一次说透。
3.1 test命令的全面拆解
test命令的基本用途是“判断一个表达式是否为真”,表达式为真返回0,为假返回1。它支持的判断类型主要有:
- 字符串判断:
-z(空判断)、-n(非空判断)、=(字符串相等)、!=(不相等)、<(字符串小于,需要转义)、>(字符串大于,需要转义) - 整数比较:
-eq、-ne、-gt、-ge、-lt、-le,分别对应等于、不等、大于、大于等于、小于、小于等于 - 文件判断:
-e(存在)、-f(是普通文件)、-d(是目录)、-r(可读)、-w(可写)、-x(可执行)、-s(文件非空)、-L(是符号链接) - 逻辑组合:
-a(逻辑与)、-o(逻辑或)、!(逻辑非)
举个例子:
bash复制if [ -f "/etc/passwd" ]; then
echo "系统密码文件存在"
fi
if [ -w "/tmp/log" ]; then
echo "日志目录可写"
fi
文件判断在生产脚本里用得极多,因为很多操作的前置条件就是“某个文件存在/可写/可执行”。比如备份脚本里先判断源目录是否存在:
bash复制if [ ! -d "/data/backup" ]; then
mkdir -p /data/backup
fi
3.2 [[]]是[]的增强版,但注意它不是POSIX标准
[[ ... ]]是Bash和Zsh提供的扩展关键字,它跟[ ... ]最核心的区别有几个:
- 变量不需要加双引号也能安全比较。
[[ $name == "hello" ]]不会因为变量为空而报错,但[ $name = "hello" ]在变量为空时会变成[ = "hello" ],直接语法错误(=: unary operator expected),你必须写成[ "$name" = "hello" ]。这是新手写[]时最常踩的坑。 - 支持正则匹配和通配符匹配。
[[ $url =~ ^https?:// ]]、[[ $file == *.log ]]这些在[]里是做不到的([]的==是纯相等符号,不支持通配符匹配)。 - 支持
&&和||作为逻辑与或,而[]里只能写-a和-o。注意[[里的&&和||,与命令间的&&和||不同,它们是在表达式内部做逻辑组合,语义更接近其他编程语言。 <和>不需要转义。在[]里写[ "a" < "b" ]会被shell重定向解析,必须写成[ "a" \< "b" ];而[[ "a" < "b" ]]直接按字符串比较处理。
我用一张表把差异列举一下:
| 比较项 | [ ] |
[[ ]] |
|---|---|---|
| 变量加双引号 | 必须,否则空变量报错 | 可以不写,也能安全处理空变量 |
| 通配符匹配 | 不支持 | 支持,[[ $f == *.log ]] |
| 正则匹配 | 不支持 | 支持,[[ $str =~ ^[0-9]+$ ]] |
&&、` |
` | |
>、<字符串比较 |
必须转义,否则被当成重定向 | 不需要转义 |
| POSIX兼容 | 是 | 否,仅Bash/Zsh |
那是不是所有场景都无脑用[[ ]]就行?也不是。如果你的脚本要跑在sh(Dash)这种轻量级shell环境里,[[ ]]是根本不支持的,会直接报语法错误。写系统启动脚本、Debian系的环境初始化脚本时,很多默认的/bin/sh就是Dash,这时候老老实实用[ ]才是兼容的做法。我的建议是:只在脚本开头明确写了#!/bin/bash时使用[[ ]],其他场景一律用[]。
3.3 (())专门做算术判断,比[]和[[]]都顺眼
除了字符串和文件,条件语句还经常需要做整数比较。虽然[ "$a" -gt "$b" ]能用,但每次写-gt、-lt总觉得不够直观。Bash提供了一种算术风格的判断方式((...)):
bash复制if (( a > b )); then
echo "a大于b"
fi
((...))里写的是C语言风格的算术表达式,支持>, <, >=, <=, ==, !=,还支持+, -, *, /, %等运算符。变量名在(())里可以省略$,直接写a就行。这个语法的退出码规则是:表达式计算结果非0,退出码为0;计算结果为0,退出码为1。因为C语言里0为假非0为真,正好反过来。
听着绕,用起来很直接。比如需要判断端口号是否在合法范围内:
bash复制if (( port >= 1 && port <= 65535 )); then
echo "端口号合法"
else
echo "端口号不合法"
fi
这比[ "$port" -ge 1 -a "$port" -le 65535 ]可读性好太多了。想在((...))里把变量展开也可以,比如if (( $a + $b > 100 )),但不建议,省略$是更地道的写法。
3.4 字符串判断和数字判断不能混淆
Shell条件判断里最让新手混淆的,就是字符串相等用=,数值相等用-eq。比如:
bash复制if [ "$age" = "18" ]; then
echo "年龄字符串是18"
fi
if [ "$age" -eq 18 ]; then
echo "计算结果是18"
fi
如果$age的值是"18",两种写法判断结果都一样。但如果有变量值是"18.0",第一种成立(字符串完全一样),第二种会报错(integer expression expected),因为-eq要求两边必须是整数。反过来,如果$age设成了" 18 "(带空格),字符串判断不相等,但数值比较时shell会做类型转换,也可能相等。所以写之前一定要想清楚:你要比的是“文本内容”还是“数值大小”。
还有一个隐藏坑:[ "$num" -gt 10 ]里如果$num是空的、或者不是纯数字,会直接报integer expression expected。因此做数值比较前最好先确认变量是整数:
bash复制if [[ "$num" =~ ^[0-9]+$ ]]; then
if [ "$num" -gt 10 ]; then
echo "数字大于10"
fi
else
echo "num不是纯数字"
fi
利用[[ ]]的正则匹配能力做前置校验,是生产脚本里常见的组合写法。
4. 实际场景中的条件组合与实践技巧
前面把语法和表达式的细节讲透了,这一节结合我在真实项目中写过的脚本片段,说一些高频出现的组合用法和容易忽略的细节,这些才是写条件语句时真正拉开差距的地方。
4.1 多条件同时判断:一旦写错,安全隐患极大
现实需求里很少只有一个判断条件,多条件组合是常态。推荐的方式是多个[]用&&和||连接,或者在[[]]里用&&和||组合。我就不推荐在[]里用-a、-o了,一是可读性差,二是兼容性在POSIX体系里也是争议点,很容易在老旧系统上翻车。
一个例子:判断一个文件是否存在且非空,才能执行后续处理。
bash复制if [ -s "/tmp/data.csv" ]; then
echo "文件存在且非空,继续处理"
else
echo "文件为空或不存在,终止"
exit 1
fi
如果要同时判断“文件存在”和“当前用户是root”,可以写成:
bash复制if [ -f "/etc/app.conf" ] && [ "$(whoami)" = "root" ]; then
echo "配置存在且有root权限"
fi
命令替换$(whoami)在条件里也能直接用。注意这里的&&和||如果混合使用要小心优先级。比如:
bash复制if [ -f "/tmp/a" ] || [ -f "/tmp/b" ] && [ "$mode" = "all" ]; then
...
fi
这条表达式的解析顺序跟多数编程语言不同,&&优先级高于||,实际含义是“(b存在且mode为all)或a存在”,很容易写出与预期不符的逻辑。为了避免这种歧义,凡是混合使用&&和||,都尽量用[[]]里的括号来明确优先级:
bash复制if [[ (-f "/tmp/a" || -f "/tmp/b") && "$mode" = "all" ]]; then
...
fi
虽然[[ ]]里不一定非得加引号,但我建议给变量加上,防止特殊字符导致意外展开。
4.2 处理变量为空和未定义的情况
这是脚本健壮性最核心的一块。变量为空和变量未定义是两回事,但在[ ]判断中表现往往相同,都可能导致条件或报错。举个最常见的场景:
bash复制if [ "$1" = "start" ]; then
echo "启动服务"
fi
如果用户没传参数,$1未定义,展开之后[ = "start" ]就是一条非法表达式,shell会提示[: =: unary operator expected。这类错误在别人的机器上跑不出来,往往就是参数没传。
处理方式是把参数先赋予默认值:
bash复制action="${1:-}" # 如果$1未定义或为空,取空字符串
if [ "$action" = "start" ]; then
echo "启动服务"
fi
或更严格地先检查参数是否传了:
bash复制if [ $# -lt 1 ]; then
echo "用法: $0 {start|stop|restart}"
exit 1
fi
$#是位置参数的个数。先判断参数数量,再判断参数内容,这是写命令行工具脚本的标配做法。
再说一个变量为空可能带来的“假阳性”问题。比如想判断一个目录环境变量是否可写:
bash复制if [ -w "$MY_DIR" ]; then
echo "目录可写"
fi
如果MY_DIR为空,[ -w "" ]会判断失败,不会报错,但输出就成了“目录不可写”,跟实际情况不符。这种问题最难排,因为不报错、只是结果不符合预期。所以无论如何,判断前先确保关键变量的值是你预期的:
bash复制if [ -z "$MY_DIR" ]; then
echo "MY_DIR未设置,终止处理"
exit 1
fi
if [ -w "$MY_DIR" ]; then
echo "目录可写"
fi
4.3 case代替多层elif,处理多值匹配是降维打击
if/elif/else虽然能处理多个分支,但如果你要对同一个变量做大量不同值匹配,代码会变得很拖沓。这种场景用case语句会清爽得多。
case的结构:
bash复制case "$1" in
start)
systemctl start myservice
;;
stop)
systemctl stop myservice
;;
restart|reload)
systemctl restart myservice
;;
*)
echo "用法: $0 {start|stop|restart|reload}"
exit 1
;;
esac
每个分支用)结尾,;;代表该分支匹配结束,*是默认分支,相当于if里的else。这里的模式支持通配符,所以可以写模糊匹配:
bash复制case "$file" in
*.tar.gz|*.tgz)
tar -xzf "$file"
;;
*.zip)
unzip "$file"
;;
*.log)
echo "日志文件,不做解压处理"
;;
*)
echo "无法识别的文件类型"
;;
esac
case还有一个很好用的场景:判断当前操作系统发行版。这个需求在初始化脚本里太常见了:
bash复制case "$(cat /etc/os-release | grep '^ID=' | cut -d= -f2)" in
ubuntu|debian)
apt-get update
;;
centos|rhel|fedora)
yum update
;;
*)
echo "不支持的系统"
exit 1
;;
esac
注意case的匹配是按顺序从上往下找第一个匹配的分支,然后用;;退出整个case。所以通配符要小心别写到精确模式前面。比如:
bash复制case "$mode" in
*)
echo "任何模式都匹配到这里"
;;
fast)
echo "永远到不了这里"
;;
esac
这个顺序错误不会报错,但后面的分支全部失效,是个隐蔽的坑。
4.4 条件语句里使用函数:让判断逻辑可以被复用
如果同一个判断逻辑在脚本里要出现多次,把它写成一个函数是最高效的做法。比如判断系统可用内存是否充足:
bash复制check_memory() {
local min_free=$1
local free_mem=$(free -m | awk '/^Mem:/{print $7}')
if [ -z "$free_mem" ]; then
echo "无法获取内存信息"
return 2
fi
if [ "$free_mem" -lt "$min_free" ]; then
echo "可用内存不足: ${free_mem}MB, 需要${min_free}MB"
return 1
fi
return 0
}
if check_memory 512; then
echo "内存充足,可以执行任务"
else
echo "内存不足,任务取消"
fi
这里我给函数设计了三种返回:0表示通过、1表示不通过、2表示无法判断。这样上层只用看退出码,就能决定是否继续。用函数封装条件的好处是,判断逻辑只写一次,以后要调整阈值,只改函数内部即可。
4.5 条件判断与重定向结合:把标准错误吞掉,让渠道更干净
写条件语句时,有些命令的报错会输出到标准错误,干扰终端或日志。比如:
bash复制if [ -r "$SOME_FILE" ]; then
cat "$SOME_FILE"
fi
如果文件不可读,并不会产生标准错误,因为[ -r ]只是判断权限,不代表后面就一定成功。但如果用grep这类命令做判断,报错信息很容易污染输出:
bash复制if grep -q "success" /var/log/app.log; then
echo "应用日志有success记录"
fi
如果日志文件不存在,grep会输出到标准错误,虽然不影响if的判断结果,但用户在终端上会看到一行错误提示,很影响体验。建议养成习惯,凡是“只关心退出码、不关心输出”的命令,一律把标准输出和标准错误都重定向到/dev/null:
bash复制if grep -q "success" /var/log/app.log 2>/dev/null; then
echo "应用日志有success记录"
fi
如果你还想把匹配到的内容留作后用,就别急着丢弃标准输出:
bash复制if result=$(grep "success" /var/log/app.log 2>/dev/null); then
echo "匹配到内容: $result"
fi
这种写法里,$(...)的结果同时被条件判断使用,又保存在result变量里,非常实用。
5. 常见问题与排查思路实录
条件语句报错排错,大多数时候不是语法记不住,而是细节没注意。这里整理几类高频的坑,附上排错思路。
5.1 语法错误:syntax error near unexpected token `then'
这类报错最常见的原因是判断条件后缺少分号。比如:
bash复制if [ "$a" = "b" ]
then
echo ok
fi
这种是合法的,因为then前面是换行。但如果想写成一行,就必须加;:
bash复制if [ "$a" = "b" ]; then echo ok; fi
还有一类是fi写错了,比如打成了fi但用了中文输入法变成fi,这类错误肉眼很难发现。排错时建议用bash -n做语法检查,不用执行就能发现语法问题:
bash复制bash -n myscript.sh
5.2 unary operator expected 或 integer expression expected
这是条件表达式里最经典的报错。看例子:
bash复制if [ $name = "hello" ]; then
echo ok
fi
如果$name为空,展开之后变成[ = hello ],[会把=当一元操作符来解析,报unary operator expected。
如果变量是空字符串但本应是数值判断,比如:
bash复制if [ $num -gt 10 ]; then
echo ok
fi
$num为空时展开成[ -gt 10 ],报integer expression expected。解决办法就是给变量加双引号,或先校验变量格式。
排错思路:先在脚本前面加一行set -x,它会打印每条命令的实际执行展开内容,你就能一眼看到到底是[ = hello ]还是[ -gt 10 ]这种问题。排完再删掉或注释掉。
5.3 unary operator expected 与 [] 里的空格数不对
[、]与条件之间必须各留一个空格。把[ "$a" = "$b" ]写成["$a" = "$b"],bash会把["$a"当做一个整体命令去找,报错变成[foo: command not found。同样[ "$a" = "$b"]这种写法,$b"]会被当作一个整体字符串,判断永远不成立,而且不报错,非常迷惑。
5.4 if判断总是走else分支,但条件看着没问题
这种问题多半是变量里带了肉眼不可见的字符。比如从文件读内容时,变量末尾带着\r(Windows换行符)、空格或制表符。[ "$var" = "expected" ]比较的是绝对字符串,多了个空格或\r就不相等了。排错时用printf '%s' "$var" | od -c或cat -A把变量输出成可见字符,马上就能发现。
还有一个常见场景:脚本从命令行参数读路径,用户传入/tmp/a/,代码里期望的是/tmp/a,尾部斜杠导致判断失败。这种情况下先做规范化处理:
bash复制dir="${1%/}" # 去掉变量末尾的/
5.5 条件语句在管道中失效:set -e和管道退出码
比如写这样一个片段:
bash复制set -e
if echo "$data" | grep -q "danger"; then
echo "检测到危险内容"
fi
如果脚本里开了set -e,在某些老版本Bash里当grep没有匹配时会直接导致整个脚本退出,而不是执行if的else逻辑。这是因为管道命令的退出码问题:echo ... | grep -q里,只要grep返回1,管道的整体退出码就是1,set -e直接把脚本赶出去了。
解决办法是在if条件里,命令的“不匹配”不再触发set -e。实际上按POSIX语义,在if条件里出现失败命令不会触发set -e,但为了保险,也可以避免管道套条件,换一种写法:
bash复制set -e
if grep -q "danger" <<< "$data"; then
echo "检测到危险内容"
fi
使用Here String避免管道,条件判断更直接。或者干脆在脚本开头用set +e暂时关闭退出检查,处理完条件再开回来。但最推荐的仍是利用if的隐式保护机制,配合set -o pipefail时要特别小心,pipefail会让管道中任何一个失败都影响整体退出码,在if条件里同样可能触发意外行为。
提示:写生产脚本时,如果发现“条件里命令一失败整个脚本就退出”,优先检查脚本头部是不是有
set -e,以及有没有开pipefail。你可以用set +e临时关闭,排查完再恢复。
5.6 正则匹配在[[]]里不生效,或语法报错
只有[[ =~ ]]支持正则,[ ]不支持。另外正则里如果有空格的字符类,比如[[:space:]],在[[ =~ ]]里面要小心引号的问题。
bash复制if [[ "$var" =~ ^[[:space:]]*$ ]]; then
echo "全是空白字符"
fi
这段写法没问题,但如果你把正则写成了"^[[:space:]]*$"(加双引号),在某些Bash版本里会被当成普通字符串比较,而不是正则匹配。所以正则部分不要加双引号。如果正则里有空格,最好用一个变量保存再判断:
bash复制pattern='^[[:space:]]*$'
if [[ "$var" =~ $pattern ]]; then
echo "全是空白字符"
fi
5.7 条件语句调试三板斧
我总结一个自己的排错顺序,碰到条件判断不正常时,按这个顺序走基本都能定位:
bash -n script.sh检查语法,排除词法层面的错误。bash -x script.sh或脚本内set -x开启执行追踪,看每条命令的展开结果,重点观察[ ... ]里变量到底展开成了什么。- 在关键变量前手动打印:
echo "DEBUG: name=[$name]",把变量值用中括号包起来,能直观看出有没有隐藏空格或换行。
其中第2步产生的调试输出比较多,适合小范围测试;第3步则适合精准定位某个具体变量。
6. 一些进阶写法和我的个人体会
条件语句写到一定阶段,你会开始追求“表达更精确”“代码更耐看”。这里分享几个我自己常用的进阶技巧,不一定所有书上都写。
6.1 用case做输入校验,比if更有安全感
当脚本需要接收用户输入时,用case做白名单校验非常稳。比如交互式脚本让用户选择“y/n”:
bash复制read -r answer
case "$answer" in
y|Y|yes|YES|Yes)
echo "继续执行"
;;
n|N|no|NO|No)
echo "退出"
exit 0
;;
*)
echo "请输入y或n"
exit 1
;;
esac
这里把大小写都列出来,总比[ "$answer" = "y" -o "$answer" = "Y" ]这种写法优雅得多。case还能很自然地做“空输入”校验:
bash复制case "$answer" in
"")
echo "输入不能为空"
exit 1
;;
...
esac
6.2 条件判断里使用数组长度和元素
Bash的数组是日常管理配置项的利器。判断数组是否有元素、是否包含某个值,这类需求也可以用条件语句解决:
bash复制apps=(nginx mysql redis)
if [ ${#apps[@]} -eq 0 ]; then
echo "应用列表为空"
fi
if [[ " ${apps[*]} " == *" mysql "* ]]; then
echo "列表中包含mysql"
fi
第二段里用空格把数组展开后的内容包起来,再用==配合通配符匹配,这是判断数组是否包含某一项的经典用法。
6.3 条件表达式与“短路求值”的配合
我平时写部署脚本,经常会用短路求值来精简逻辑:
bash复制command -v curl >/dev/null 2>&1 || { echo "curl未安装"; exit 1; }
command -v检查命令是否存在,后面的||就是说“如果检查失败,就打印提示并退出”。它跟if/else是等价的,但更紧凑。类似地还有“缺省值赋值”:
bash复制: "${ENV_FILE:=/etc/default/app}"
这行不是条件判断,但:=的赋值语法默认在变量为空时给默认值。跟条件语句配合时,能省掉很多啰嗦的if [ -z "$VAR" ]; then VAR=...; fi。
6.4 条件语句不止在脚本里用——交互shell里也能大幅提效
很多读者只在写脚本时才想到条件语句,但你在终端里手动敲命令时,条件语句同样能帮你。比如我想看日志目录里文件数是否超过100个,再决定要不要压缩:
bash复制count=$(ls /var/log/myapp/ | wc -l)
if (( count > 100 )); then
tar -czf /tmp/logs_$(date +%F).tar.gz /var/log/myapp/
fi
当然这只是一个示意,实际写日志清理脚本要更复杂,但核心思想是相通的。把常用操作写成函数塞进~/.bashrc里,配合条件判断,等于给终端加了一层自动决策能力。
6.5 关于嵌套层数的一些建议
条件语句可以嵌套,比如:
bash复制if [ -f "/tmp/a.txt" ]; then
if [ -s "/tmp/a.txt" ]; then
echo "文件存在且非空"
else
echo "文件存在但为空"
fi
else
echo "文件不存在"
fi
嵌套本身没问题,但嵌套层数一多,代码可读性会急剧下降。我个人的经验是:
- 嵌套超过3层时,优先考虑提取成函数。
- 能写成“先校验再执行”的,就不要把校验条件层层套起来。比如把无效情况提前
exit 1,后续的代码就一直在“已通过校验”的直线流程里跑,这种风格叫卫语句(guard clause)。
bash复制if [ ! -f "/tmp/a.txt" ]; then
echo "文件不存在,终止" >&2
exit 1
fi
if [ ! -s "/tmp/a.txt" ]; then
echo "文件为空,终止" >&2
exit 1
fi
echo "文件存在且非空,继续处理"
同样的逻辑,用卫语句写比嵌套更容易读懂,也更不容易出错。
7. 收个尾:条件语句的学习路线参考
条件语句是shell编程里少有的“看起来简单、写起来翻车率却极高”的内容。如果你真要系统掌握,我的建议是按下面的顺序来:
- 先吃透退出码机制,明白if判断的底层原理,这一步是地基。
- 把test的常用选项练熟,尤其是
-f、-d、-z、-n这些高频文件与字符串判断。 - 熟练使用
[]并养成变量加双引号的习惯,这能避开七八成的低级bug。 - 切换Bash环境时引入
[[]],体会它与[]的差异。 - 用case重构一个自己写过的多层elif脚本,感受多值匹配的优雅。
- 在项目中刻意练习函数封装+条件判断的组合,让脚本逻辑可以被复用。
我个人在实际操作中最深的体会是,条件语句写得好不好,不在语法记得多熟,而在于每写一个判断前都想清楚“如果变量为空会怎样,如果文件不存在会怎样,如果上一条命令失败了会怎样”。大部分线上事故,不是条件判断不会写,而是把“正常情况下成立”当成了“永远成立”。多问自己几个边界问题,多写几个卫语句,你写出来的脚本自然会比大多数人稳。
