Shell脚本的决策智慧:if、case及测试条件全解析
一个强大的 Shell 脚本不仅能按顺序执行命令,更应该具备根据不同情况做出不同决策的能力。这就是条件判断语句的魅力所在。本文将带你深入理解 Shell 脚本中最核心的
和
if
语句,以及它们赖以工作的各种测试条件。
case
一、
if
语句:脚本的逻辑核心
if
语句是最基本也是最常用的条件判断结构。它的核心思想是:如果某个条件成立,那么就执行某些操作。
if
1. 基本语法:
if ... then ... fi
if ... then ... fi
这是最简单的
结构。
if
#!/bin/bash
# 检查当前用户是否是 root
if [ "$(whoami)" == "root" ]; then
echo "警告:你正在以 root 用户身份运行此脚本!"
fi
echo "脚本继续执行..."
解读:
后面跟着一个测试条件
if
。如果该条件为真(命令返回退出码 0),
[ "$(whoami)" == "root" ]
和
then
之间的代码块就会被执行。无论条件是否成立,脚本都会继续执行
fi
之后的代码。
fi
2.
if-else
结构:两种选择
if-else
当你需要在条件成立和不成立时执行不同操作,可以使用
。
if-else
#!/bin/bash
LOG_FILE="/var/log/app.log"
if [ -w "$LOG_FILE" ]; then
echo "日志文件可写,正在记录日志..."
date >> "$LOG_FILE"
else
echo "错误:日志文件 $LOG_FILE 不可写!"
exit 1
fi
解读:如果文件可写(
测试条件),则执行
-w
块;否则,执行
then
块。
else
3.
if-elif-else
结构:多种选择
if-elif-else
当你需要检查一系列互斥的条件时,
(else if) 就派上用场了。
elif
#!/bin/bash
read -p "请输入一个分数 (0-100): " score
if [ "$score" -ge 90 ]; then
echo "优秀 (A)"
elif [ "$score" -ge 80 ]; then
echo "良好 (B)"
elif [ "$score" -ge 60 ]; then
echo "及格 (C)"
else
echo "不及格 (F)"
fi
解读:脚本会从上到下依次检查条件,一旦某个
或
if
的条件满足,就会执行其对应的代码块,然后直接跳到
elif
,不再检查后面的
fi
或
elif
。
else
二、测试条件:
if
语句的大脑
if
语句本身只是一个框架,真正让它工作的是方括号
if
[
或
]
[[
中的测试条件。
]]
重要提示:在现代 Bash 脚本中,强烈推荐使用
,因为它更健壮、功能更强,能避免很多
[[ ... ]]
的常见错误。
[ ... ]
1. 文件与目录测试
操作符 | 描述 | 示例 |
---|---|---|
|
文件或目录是否存在 |
|
|
是否为一个普通文件 |
|
|
是否为一个目录 |
|
|
是否可读 |
|
|
是否可写 |
|
|
是否可执行 |
|
|
文件是否存在且非空 |
|
2. 字符串测试
操作符 | 描述 | 示例(推荐使用 ) |
---|---|---|
或
|
字符串内容是否相等 |
|
|
字符串内容是否不相等 |
|
|
字符串是否为空(长度为零) |
|
|
字符串是否不为空 |
|
3. 整数比较测试
操作符 | 描述 | 示例 |
---|---|---|
|
等于 (equal) |
|
|
不等于 (not equal) |
|
|
大于 (greater than) |
|
|
大于或等于 (greater or equal) |
|
|
小于 (less than) |
|
|
小于或等于 (less or equal) |
|
4. 逻辑组合
:逻辑与 (AND) –
&&
[[ $age -ge 18 && "$country" == "CN" ]]
:逻辑或 (OR) –
||
[[ "$role" == "admin" || "$role" == "root" ]]
:逻辑非 (NOT) –
!
(如果 lock.file 不存在)
[[ ! -f "lock.file" ]]
5. if 高级使用
语句的威力取决于你如何运用测试条件。我们推荐使用
if
,因为它更健壮、功能更强。
[[ ... ]]
核心技巧:使用
进行逻辑非
!
操作符可以反转任何测试条件的结果。这在“如果不存在则…”的场景中非常有用。
!
: 如果 “file.txt” 不是一个文件(或不存在),则条件为真。
[[ ! -f "file.txt" ]]
: 如果 “/tmp/data” 不是一个目录,则条件为真。
[[ ! -d "/tmp/data" ]]
: 如果
[[ ! "$user" == "root" ]]
的值不等于 “root”,则条件为真。
$user
CONFIG_FILE="app.conf"
if [[ ! -r "$CONFIG_FILE" ]]; then
echo "错误:配置文件 $CONFIG_FILE 不存在或不可读!"
exit 1
fi
6. 高级技巧:用
&&
和
||
实现简洁的控制流
&&
||
在 Shell 中,命令的成功与否由其退出码决定(0 代表成功,非 0 代表失败)。我们可以利用这一点,通过逻辑与
和逻辑或
&&
来构建极其简洁的命令链,从而替代简单的
||
结构。
if-then-else
: “与”链。只有当
command1 && command2
成功时(退出码为 0),才会执行
command1
。
command2
: “或”链。只有当
command1 || command2
失败时(退出码非 0),才会执行
command1
。
command2
实战解析:一行代码的智慧
让我们来解构你提到的那段惊艳的脚本:
[ ! -f density.rst ] && { ${MDRUN_AMBER} ... > log 2>&1; } || { { mpirun ... >> log2 2>&1; } || { echo "Failed..." && exit; } }
这段代码看似复杂,但遵循着清晰的逻辑:尝试A,如果A失败,则尝试B,如果B也失败,则报错。
[ ! -f density.rst ] && ...
判断:如果文件
不存在…动作:
density.rst
意味着,只有在上述判断为真(文件确实不存在)时,才会执行后面的整个大括号
&&
命令组。
{...}
{ ${MDRUN_AMBER} ...; } || ...
首次尝试:执行
中的
{...}
命令。
MDRUN_AMBER
将多个命令组合成一个逻辑单元。
{ ...; }
表示将标准输出和标准错误都重定向到日志文件。失败后备:
> MD_mdrun.log 2>&1
意味着,如果
||
命令失败了(返回了非零退出码),则执行
MDRUN_AMBER
后面的命令组。
||
{ { mpirun ...; } || { echo "Failed..." && exit; } }
二次尝试:执行
命令。最终失败处理:
mpirun
意味着,如果
||
命令也失败了,就执行最后的
mpirun
部分:打印错误信息
{...}
并且 (
echo "Failed..."
) 立即退出脚本
&&
。
exit
总结:这行代码优雅地实现了一个健壮的执行链:检查前提 -> 尝试主方案 -> 尝试备用方案 -> 最终错误处理。这种写法在需要确保关键步骤必须成功时非常有用。
三、
case
语句:
if
的优雅替代品
case
if
当你的判断条件都围绕同一个变量的多种可能值时,使用
语句会比冗长的
case
链条更加清晰易读。
if-elif
1.
case
语法
case
语句将一个变量的内容与多个模式(pattern)进行匹配。
case
#!/bin/bash
# 一个简单的服务控制脚本
ACTION="$1" # 第一个命令行参数
case "$ACTION" in
start)
echo "正在启动服务..."
# 启动服务的命令
;;
stop)
echo "正在停止服务..."
# 停止服务的命令
;;
restart)
echo "正在重启服务..."
# 重启服务的命令
;;
status)
echo "正在检查服务状态..."
# 检查状态的命令
;;
*) # 匹配所有其他情况 (默认选项)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
解读:
:开始
case "$ACTION" in
语句,判断
case
变量的值。
$ACTION
:一个模式。如果
start)
的值是 “start”,则执行其下的代码。
$ACTION
:每个代码块的结束符,类似于 C 语言中的
;;
。
break
:一个通配符模式,用于捕获所有未被前面模式匹配到的值,通常作为默认或错误处理分支。
*)
:
esac
语句的结束标记(
case
的倒写)。
case
2. 高级应用:
while
+
case
+
shift
构建专业参数解析器
while
case
shift
要处理
或
-p 123
这样的命令行参数,我们需要一个循环来遍历所有参数。
--user admin
、
while
和
case
的组合是实现此功能的黄金标准。
shift
代码范例:
#!/bin/bash
# 默认值
concentration=""
min_distance=""
PDBID=""
boundary=""
forcefield=""
# 帮助函数
show_help() {
echo "用法: $0 [选项]..."
echo " -c, --concentration 设置浓度"
echo " -d, --distance 设置最小距离"
# ... 其他帮助信息
}
# --- 核心解析循环 ---
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--concentration)
concentration="$2" # 将选项的值($2)赋给变量
shift 2 # 消耗掉选项(-c)和它的值,共2个参数
;;
-d|--distance)
min_distance="$2"
shift 2
;;
-p|--pdbid)
PDBID="$2"
shift 2
;;
-h|--help)
show_help # 调用帮助函数
exit 0 # 正常退出
;;
*)
echo "未知参数: $1"
exit 1
;;
esac
done
echo "--- 解析结果 ---"
echo "浓度: ${concentration:-未设置}"
echo "距离: ${min_distance:-未设置}"
echo "PDB ID: ${PDBID:-未设置}"
# 脚本后续逻辑...
智慧解析:
: 只要还存在命令行参数(
while [[ $# -gt 0 ]]
代表参数总数),循环就继续。
$#
:
case "$1" in
语句负责判断当前第一个参数 (
case
) 是哪个选项。
$1
: 这是模式匹配的精髓。
-c|--concentration)
代表“或”,因此这个模式可以同时匹配短选项
|
和长选项
-c
。
--concentration
: 如果匹配成功,脚本认为紧跟在选项后面的第二个参数 (
concentration="$2"
) 就是这个选项的值,并将其赋给相应的变量。
$2
: 这是最关键的一步。
shift 2
命令会丢弃掉位置参数。
shift
会丢弃掉
shift 2
和
$1
,然后原来的
$2
变成新的
$3
,原来的
$1
变成新的
$4
,以此类推。这样,下一次循环开始时,
$2
就已经是下一个待处理的选项了。对于像
$1
这样不带值的选项,我们只需
-h
(或
shift
) 一次。
shift 1
三、结论:如何选择?
使用
语句:
if
进行复杂的逻辑判断(数值范围、多变量、文件属性)。当你需要利用
和
&&
构建简洁的、依赖于命令成功/失败的执行链时。
||
使用
语句:
case
当你的判断逻辑基于单个变量的固定模式匹配时,它比
更清晰。与
if-elif
和
while
结合,成为解析命令行参数的不二之选。
shift
通过掌握这些从基础到高级的决策工具,你将能够编写出逻辑严密、健壮可靠且用户体验良好的专业级 Shell 脚本。