1. 为什么一个看似简单的 if else 总在凌晨三点把你叫醒
“if(加班)printf 在家吃 else 出去吃”——这句热梗刷屏时,我正蹲在服务器机柜前,手里攥着一张被咖啡渍晕染的打印纸,上面是刚跑崩的部署脚本里第7次出错的条件判断段。不是语法报错,没有红色提示,它只是安静地跳过了该执行的备份逻辑,把生产数据库的每日快照悄悄删掉了。等监控告警响起,已经是凌晨三点十七分。
这就是 Shell 中 if else 的真实面目:它不像 Python 的缩进或 Java 的大括号那样有强视觉约束,也不像 Go 那样强制要求花括号包裹单行语句。它的语法骨架极简——
if [ condition ]; then ... else ... fi
,但骨架之下全是暗流。一个空格的位置、一个未引号包裹的变量、一次对命令退出码的误读,都可能让整个逻辑链在你眼皮底下无声断裂。而最致命的是,Shell 不会主动告诉你“这里可能出问题”,它只会用静默失败(silent failure)给你留个坑,等你半夜爬起来填。
我见过太多人把 if else 当作“会写就行”的基础语法,结果在自动化运维、CI/CD 流水线、日志轮转、服务健康检查这些关键场景里栽跟头。有人因为
[ $var = "yes" ]
中
$var
为空导致测试变成
[ = "yes" ]
,直接语法错误中断脚本;有人用
if grep -q "error" /var/log/app.log
却没意识到 grep 找不到匹配时返回 1(非零),被误判为“有错误”而触发误告警;还有人把
[[ ]]
和
[ ]
混用,在不同 shell 版本间迁移时发现正则匹配突然失效……这些都不是理论风险,而是我亲手修过、复盘过、写进团队 Wiki 的真实故障快照。
所以这篇不是“教你怎么打字”的入门手册。它是从十年线上脚本维护现场抠出来的实战图谱:
什么时候必须用
[[ ]]
而不是
[ ]
?为什么
if command; then
比
if [ $(command) ]
更可靠?如何设计一个永不因空值崩溃的条件分支?当你的脚本要跑在 CentOS 7 的 bash 4.2 和 macOS 的 zsh 5.8 上时,哪些写法是跨平台安全的?
我们不讲抽象规则,只拆解每一个字符背后的执行逻辑,用真实命令输出、错误日志截图(文字还原)、以及修复前后对比,带你把 if else 从“能用”变成“敢放生产环境用”。
关键词早已隐含在热梗里: Shell Script 是载体, if else 是核心动作, Comprehensive Guide 意味着覆盖所有边界, Examples 则必须是能直接粘贴进你脚本里、改个路径就能跑通的实例。现在,我们从最基础却最易错的语法结构开始。
2. 语法骨架的三重陷阱:空格、分号与退出码的本质
Shell 的 if 语句表面看是四块积木:
if
、
then
、
else
、
fi
。但真正决定逻辑生死的,是夹在它们之间的三个隐形关节:
条件表达式的空格规则、分号的语法位置、以及命令退出码的二元真相
。这三者任何一个松动,整个结构就会坍塌。
2.1 空格:不是风格问题,是语法铁律
初学者常犯的错误是把
[
当作关键字,其实它是系统内置的一个命令(
/usr/bin/[
或
/bin/[
),和
ls
、
cp
地位相同。这意味着
[
后面
必须跟一个空格
,否则 shell 会把它识别为一个名为
[something
的命令,自然报错
command not found
。
# ❌ 错误:[ 和字符串之间无空格
if [$USER = "root"]; then
echo "You are root"
fi
# 输出:bash: [$USER: command not found
# ✅ 正确:[ 后、] 前都必须有空格
if [ "$USER" = "root" ]; then
echo "You are root"
fi
更隐蔽的陷阱在变量展开。
$USER
如果为空,
[ $USER = "root" ]
会变成
[ = "root" ]
,此时
[
命令收到两个参数:
=
和
"root"
,但
[
要求第一个参数是操作符(如
=
、
-n
),于是报错
[: =: unary operator expected
。解决方案是
永远用双引号包裹变量
:
# ❌ 危险:变量未引号,空值时崩溃
if [ $HOME = "/root" ]; then
echo "Home is /root"
fi
# ✅ 安全:引号确保参数数量稳定
if [ "$HOME" = "/root" ]; then
echo "Home is /root"
fi
提示:
[[ ]]内部对空格宽容度更高,但[ ]的空格规则是 POSIX 标准,跨 shell 兼容性更强。我的经验是: 只要用[ ],就把引号当作呼吸一样自然——不加引号的变量,就像没系安全带开车。
2.2 分号:分隔符还是命令终结符?
if [ condition ]; then
中的分号,常被误解为“可有可无”。实际上,它在这里是
命令分隔符
,作用是告诉 shell:“前面的
[ condition ]
是一个独立命令,现在我要开始写下一个命令
then
了”。你可以用换行替代分号,效果完全相同:
# 两种写法完全等价
if [ "$?" -eq 0 ]; then
echo "Success"
fi
if [ "$?" -eq 0 ]; then echo "Success"; fi
但分号的关键价值在于 单行脚本的紧凑性 。比如在 CI 脚本中快速判断:
# 一行完成:检查文件存在且非空,存在则上传
[ -s "build.tar.gz" ] && curl -F "file=@build.tar.gz" https://upload.example.com
这里
&&
是逻辑运算符,而
[ ]
后的分号在单行中让结构更清晰。不过要注意:
then
后面
不能
加分号,因为
then
是关键字,不是命令,分号会破坏语法。
2.3 退出码:Shell 逻辑的唯一真理
Shell 中 if 的本质,不是判断“某个值是否为真”,而是判断
前一个命令的退出码(exit code)是否为 0
。这是所有困惑的根源。
[ ]
命令本身就是一个程序,它根据条件是否成立返回 0(成功/真)或 1(失败/假)。但很多命令的退出码含义与直觉相反:
| 命令 | 条件成立时退出码 | 条件不成立时退出码 | 实际含义 |
|---|---|---|---|
[ -f file ]
| 0 | 1 | 文件存在且为普通文件 → 成立 |
grep "pattern" file
| 0 | 1 | 找到匹配行 → 成立 |
ping -c1 google.com
| 0 | 1 | 网络可达 → 成立 |
false
| 1 | — | 永远失败 |
true
| 0 | — | 永远成功 |
关键认知:
if 判断的是命令执行结果,不是命令输出内容
。所以
if echo "hello"
永远为真(
echo
总是返回 0),而
if grep "notfound" /dev/null
永远为假(
grep
无匹配时返回 1)。
实操验证:
# 查看命令退出码:$? 存储上一条命令的退出码
$ [ "a" = "a" ]; echo $?
0
$ [ "a" = "b" ]; echo $?
1
$ grep "hello" /dev/null; echo $?
1
$ echo "hello world" | grep "hello"; echo $?
0
注意:
[[ ]]是 bash/zsh 的扩展,支持正则匹配=~和模式匹配==,但其退出码逻辑与[ ]一致——只看 0 或非 0。跨平台脚本优先用[ ],复杂逻辑再考虑[[ ]]。
3. 条件表达式实战:从文件检测到字符串处理的完整武器库
if 语句的威力,90% 取决于你能否写出精准、健壮的条件表达式。
[ ]
和
[[ ]]
提供了两套工具箱,前者是 POSIX 标准,后者是 bash/zsh 增强。我们按使用频率和风险等级,逐个拆解真实场景中的写法。
3.1 文件与目录检测:生产环境的高频需求
运维脚本中,80% 的 if 用于判断文件状态。
[ ]
的
-X
系列选项是基石,但每个都有坑:
| 选项 | 含义 | 常见误用 | 安全写法 |
|---|---|---|---|
-f file
| 文件存在且为普通文件 | 忘记检查父目录是否存在 |
[ -d "$(dirname "$file")" ] && [ -f "$file" ]
|
-d dir
| 目录存在 | 对软链接目录判断失败 |
[ -d "$dir" ] && [ -L "$dir" ]
(需额外判断)
|
-s file
| 文件存在且大小 > 0 | 空文件导致后续命令失败 | `[ -s "$log" ] |
-r file
| 文件可读 | 权限变化导致脚本中断 |
if [ -r "$config" ]; then source "$config"; else echo "Config unreadable"; exit 1; fi
|
-w file
| 文件可写 | 误判 NFS 挂载点权限 |
结合
touch "$file.test" 2>/dev/null && rm "$file.test"
实测
|
真实案例:日志轮转脚本中,需要判断旧日志是否已压缩并删除:
# ❌ 危险:只检查 .gz 文件存在,不确认是否为普通文件
if [ -e "$old_log.gz" ]; then
rm "$old_log.gz"
fi
# ✅ 安全:三重校验——存在、是文件、非空
if [ -f "$old_log.gz" ] && [ -s "$old_log.gz" ]; then
echo "Compressed log exists and is non-empty, removing..."
rm "$old_log.gz"
else
echo "No valid compressed log to remove"
fi
3.2 字符串比较:引号、空值与模式匹配的生死线
字符串操作是脚本中最易出错的部分。
[ ]
只支持
=
(等价于
==
)和
!=
,而
[[ ]]
支持
==
(模式匹配)和
=~
(正则匹配)。
空值陷阱的终极解法:
# ❌ 危险:未引号变量在空值时语法崩溃
if [ $INPUT = "start" ]; then
start_service
fi
# ✅ 方案1:引号 + 显式空值检查(推荐)
if [ -n "$INPUT" ] && [ "$INPUT" = "start" ]; then
start_service
fi
# ✅ 方案2:用 [[ ]] 的模式匹配(bash/zsh 专用)
if [[ "$INPUT" == "start" ]]; then
start_service
fi
正则匹配实战:
# 检查版本号格式是否为 x.y.z(数字+点)
VERSION="1.2.3"
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Valid version format"
else
echo "Invalid version: $VERSION"
exit 1
fi
# 检查主机名是否以 prod- 开头(忽略大小写)
HOSTNAME=$(hostname)
if [[ "${HOSTNAME,,}" == prod-* ]]; then # ${var,,} 转小写
echo "Production server detected"
# 加载生产配置
CONFIG_FILE="/etc/app/prod.conf"
fi
经验: 正则匹配优先用
[[ ]],简单相等用[ ]。[[ ]]的=~运算符在 bash 3.2+ 支持,zsh 也兼容,但 dash/sh 不支持。若需最大兼容性,用case语句替代:case "$INPUT" in start|START) start_service ;; stop|STOP) stop_service ;; *) echo "Unknown command: $INPUT" ;; esac
3.3 数值比较:避免字符串陷阱的硬核技巧
Shell 中数值比较必须用
-eq
,
-ne
,
-lt
,
-le
,
-gt
,
-ge
,
绝不能用
==
或
>
,否则会被当作字符串比较:
# ❌ 字符串比较:10 < 2 为真(因为 '1' < '2')
if [ 10 < 2 ]; then
echo "This will print! (Wrong!)"
fi
# ✅ 数值比较:10 小于 2 为假
if [ 10 -lt 2 ]; then
echo "Never prints"
else
echo "10 is not less than 2"
fi
# ✅ 使用 (( )) 进行算术计算(bash/zsh)
if (( 10 > 2 )); then
echo "Arithmetic comparison works"
fi
获取进程数并判断:
# 获取 nginx 进程数
NGINX_COUNT=$(pgrep -f "nginx: master" | wc -l)
# ❌ 危险:未检查 pgrep 是否失败(如 nginx 未运行,pgrep 返回 1,wc -l 输出 0,但 $? 是 1)
if [ $NGINX_COUNT -eq 0 ]; then
echo "Nginx not running"
fi
# ✅ 安全:先检查命令是否成功,再判断数值
if NGINX_COUNT=$(pgrep -f "nginx: master" | wc -l) && [ "$NGINX_COUNT" -eq 0 ]; then
echo "Nginx not running"
elif [ "$NGINX_COUNT" -gt 0 ]; then
echo "Nginx running with $NGINX_COUNT processes"
fi
4. 复杂逻辑构建:嵌套、多分支与命令链的工程化实践
真实脚本中,if 很少是孤立的。它常嵌套在循环里,与
&&
/
||
链接,或作为函数返回值的判断依据。这些组合带来指数级的复杂度,也埋下更多雷区。
4.1 嵌套 if:何时该用,何时该重构
嵌套 if 的典型场景是“先检查前提,再检查细节”。例如部署前验证:
# ❌ 深度嵌套:可读性差,错误处理分散
if [ -f "config.yaml" ]; then
if [ -r "config.yaml" ]; then
if [ -s "config.yaml" ]; then
if grep -q "database:" "config.yaml"; then
deploy_app
else
echo "Missing database config"
exit 1
fi
else
echo "Config file is empty"
exit 1
fi
else
echo "Config file not readable"
exit 1
fi
else
echo "Config file not found"
exit 1
fi
重构为扁平化防御式编程:
# ✅ 扁平化:每个检查失败立即退出,逻辑线性清晰
[ -f "config.yaml" ] || { echo "ERROR: config.yaml not found"; exit 1; }
[ -r "config.yaml" ] || { echo "ERROR: config.yaml not readable"; exit 1; }
[ -s "config.yaml" ] || { echo "ERROR: config.yaml is empty"; exit 1; }
grep -q "database:" "config.yaml" || { echo "ERROR: database section missing in config.yaml"; exit 1; }
# 所有前置检查通过,执行主逻辑
deploy_app
技巧:
|| { commands; }是 bash 的短路特性,{ }中的命令在左侧失败时执行。比if ! condition; then ... fi更简洁,且避免了嵌套缩进。
4.2 elif 链:多路分支的清晰表达
elif
是处理多个互斥条件的利器。关键原则是:
条件顺序必须符合业务逻辑优先级
。例如服务状态检查:
# 检查服务状态并给出精确反馈
SERVICE_STATUS=$(systemctl is-active nginx 2>/dev/null)
case "$SERVICE_STATUS" in # 用 case 替代长 elif 链更清晰
active)
echo "Nginx is running"
;;
inactive|failed)
echo "Nginx is not running ($SERVICE_STATUS)"
;;
*)
echo "Unknown status: $SERVICE_STATUS"
;;
esac
但若必须用
elif
,注意:
# ✅ 条件从具体到宽泛
if [ "$ENV" = "prod" ]; then
DB_HOST="db-prod.internal"
elif [ "$ENV" = "staging" ]; then
DB_HOST="db-staging.internal"
elif [ "$ENV" = "dev" ] || [ "$ENV" = "local" ]; then
DB_HOST="localhost"
else
echo "Unknown environment: $ENV"
exit 1
fi
4.3 命令链:&& 和 || 的工程化用法
&&
和
||
是 Shell 的逻辑运算符,但它们不是 if 的替代品,而是
命令执行控制流
。滥用会导致逻辑混乱:
# ❌ 危险:逻辑歧义(如果 cmd1 失败,cmd2 不执行,cmd3 仍执行!)
cmd1 && cmd2 || cmd3 # 等价于 if cmd1; then cmd2; else cmd3; fi
# ✅ 清晰:用 if 明确意图
if cmd1; then
cmd2
else
cmd3
fi
# ✅ 安全:单行快捷写法(仅用于简单场景)
[ -f "$file" ] && cp "$file" /backup/ || echo "File missing: $file"
生产级命令链模式:
# 模式1:确保前置步骤成功才继续(类似事务)
make build && \
make test && \
make deploy || { echo "Build pipeline failed"; exit 1; }
# 模式2:错误时执行清理(trap 更优,但简单脚本可用)
rm -f "$temp_file" && \
tar -cf "$archive.tar" "$source_dir" && \
gzip "$archive.tar" || { echo "Archive failed, cleaning up"; rm -f "$archive.tar"; exit 1; }
经验: 超过3个命令的链式操作,必须用 if 重构 。
&&/||适合单行快捷判断,复杂逻辑交给 if。
5. 跨平台兼容性与调试:让脚本在任何 Linux/macOS 上稳如磐石
写完脚本只是开始,让它在不同环境中稳定运行才是挑战。CentOS 的 bash 4.2、Ubuntu 的 dash、macOS 的 zsh,对语法的支持差异巨大。
5.1 POSIX 兼容性:编写可移植脚本的黄金法则
POSIX 是 Shell 脚本的“最低公分母”。遵循它,你的脚本能在
/bin/sh
下运行(dash、ash、busybox sh):
-
永远用
[ ]而非[[ ]]:[[ ]]是 bash/zsh 扩展,dash 不支持。 -
避免
$(())算术扩展 :用$(( ))替代,或用expr(但expr已过时)。 -
字符串比较用
=而非==:==是[[ ]]特性。 -
数组?别用
:POSIX sh 不支持数组,用位置参数
$1,$2或临时文件替代。
POSIX 兼容的版本检查脚本:
#!/bin/sh
# This script runs on /bin/sh (dash, ash, busybox)
# Check if command exists (POSIX way)
if command -v curl >/dev/null 2>&1; then
DOWNLOADER="curl -fsSL"
elif command -v wget >/dev/null 2>&1; then
DOWNLOADER="wget -qO-"
else
echo "ERROR: Neither curl nor wget found"
exit 1
fi
# Download using the available tool
$DOWNLOADER "https://example.com/script.sh" > /tmp/script.sh
5.2 调试技巧:从
set -x
到退出码追踪的完整链路
调试 if 语句,核心是 可视化每一步的执行路径和数据状态 。
方法1:
set -x
(最常用)
#!/bin/bash
set -x # 开启调试,打印每条执行的命令及变量值
FILE="/tmp/test.txt"
if [ -f "$FILE" ]; then
echo "File exists"
fi
set +x # 关闭调试
输出:
+ FILE=/tmp/test.txt
+ '[' -f /tmp/test.txt ']'
+ echo 'File exists'
File exists
+ set +x
方法2:显式打印退出码
[ -f "$FILE" ]
echo "Exit code: $?" # 直接查看上一条命令结果
方法3:条件表达式分解调试
# 将复杂条件拆成变量,逐个验证
IS_FILE="[ -f \"$FILE\" ]"
IS_READABLE="[ -r \"$FILE\" ]"
echo "Testing: $IS_FILE"
eval $IS_FILE
echo "Exit code: $?"
echo "Testing: $IS_READABLE"
eval $IS_READABLE
echo "Exit code: $?"
经验: 调试时,永远先验证条件表达式本身 。在终端直接运行
[ -f "$FILE" ] && echo "yes" || echo "no",比在脚本里猜更高效。
5.3 常见故障排查表:从报错信息反推根因
| 报错信息 | 可能原因 | 快速验证命令 | 解决方案 |
|---|---|---|---|
[: =: unary operator expected
|
变量为空,导致
[ = "val" ]
|
echo "[$VAR]"
|
用
"$VAR"
引号包裹
|
[: too many arguments
| 变量含空格未引号,被拆成多参数 |
set -x
查看展开
| 引号包裹所有变量 |
command not found
|
[
后无空格,或
]
前无空格
|
type [
|
检查
[
周围空格
|
unexpected EOF while looking for matching \
'`
| 引号未闭合 |
grep "'" script.sh
| 用编辑器检查引号配对 |
syntax error near unexpected token 'elif'
|
elif
前缺少
then
或
fi
|
grep -n "if|elif|else|fi" script.sh
| 检查 if 结构完整性 |
最后分享一个血泪教训:某次在 macOS 上测试通过的脚本,上线到 CentOS 后崩溃。原因是用了
[[ "$str" =~ regex ]]
,而 CentOS 6 的 bash 3.2 不支持
=~
。解决方案是
在脚本开头强制指定解释器并检查版本
:
#!/bin/bash
# Check bash version for =~ support
if (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 2) )); then
echo "ERROR: Bash 3.2+ required for regex support"
exit 1
fi
6. 高级模式与避坑清单:十年踩坑总结的 12 条军规
经过数百个生产脚本的淬炼,我提炼出 12 条 if else 使用军规。它们不是教科书规则,而是凌晨三点修复故障后写在笔记本上的血色笔记。
6.1 军规 1-4:关于变量与引号
-
所有变量必须双引号包裹,除非你明确需要单词分割
cp "$SRC" "$DST"是铁律。唯一例外:for i in $LIST(此时需分割),但更安全写法是for i in $LIST; do或用数组。 -
空值检查永远放在字符串比较前
[ -n "$VAR" ] && [ "$VAR" = "value" ]比[ "$VAR" = "value" ]多一次判断,但避免了崩溃。 -
用
[[ ]]时,[[ $VAR ]]等价于[[ -n $VAR ]],但[ $VAR ]不等价于[ -n $VAR ]
因为[ $VAR ]在$VAR为空时变成[ ],而[ ]返回 1(假),但[ -n "" ]明确返回 1。语义更清晰。 -
路径拼接用
"$DIR/$FILE",不用"$DIR/$FILE"
如果$DIR以/结尾,"$DIR/$FILE"会变成//path/file,多数命令可处理,但某些工具(如 rsync)会出错。安全写法:"$DIR/${FILE#/}"(${FILE#/}去除开头/)。
6.2 军规 5-8:关于命令与退出码
-
永远不要依赖
$(command)的输出做 if 判断,除非你捕获了退出码
if [ "$(grep ...)" ]; then是毒药——grep失败时输出为空,[ ""]返回真!正确:if grep -q ...; then。 -
if command; then比if [ $(command) ]; then更可靠
前者直接用命令退出码,后者多一层命令替换,且易受空格/换行干扰。 -
用
||做错误处理时,确保右侧命令返回 0
cmd || echo "fail"中,echo总是返回 0,这没问题;但cmd || rm -rf /中,rm失败会继续执行,应写成cmd || { echo "fail"; exit 1; }。 -
set -e不是万能的,它不会捕获管道中的错误
set -e让脚本在命令失败时退出,但cmd1 | cmd2中,只有cmd2的退出码被检查。需用set -o pipefail。
6.3 军规 9-12:关于结构与工程实践
-
if 块内不要写超过 5 行逻辑,复杂逻辑抽成函数
# ❌ 拒绝 if [ "$ENV" = "prod" ]; then # 10 行数据库配置 # 8 行缓存设置 # 5 行监控参数 fi # ✅ 推荐 if [ "$ENV" = "prod" ]; then load_prod_config fi -
用
case替代长elif链,尤其涉及字符串匹配
case "$CMD" in start) ;; stop) ;; restart) ;; *) ;; esac比 10 个elif更易读、更高效。 -
脚本开头添加 shebang 并指定解释器版本
#!/usr/bin/env bash比#!/bin/bash更可移植,但若需特定版本,用#!/usr/bin/env bash4(需系统有 bash4)。 -
测试你的 if 逻辑:用
bash -n script.sh检查语法,用shellcheck script.sh发现潜在问题
shellcheck是 Shell 脚本的 lint 工具,能发现未引号变量、危险的$(...)等。把它加入 CI 流程。
最后,回到那个热梗:“if(加班)printf 在家吃 else 出去吃”。它之所以火,是因为道出了程序员与条件判断的永恒关系——
我们写的不是代码,是生活决策的自动化映射
。而 if else 的终极价值,不是让机器执行分支,而是让我们在写下的每一行中,都提前预演了所有可能性,并为每一个“否则”准备好了退路。这大概就是深夜改完脚本,看到
echo "Deployment successful"
时,那杯冷掉的咖啡还值得喝一口的原因。
562

被折叠的 条评论
为什么被折叠?



