在Shell脚本编程中,条件判断是最基础也是最重要的功能之一。if语句配合各种条件测试,可以实现程序的流程控制。很多初学者在刚开始接触Shell脚本时,常常对多条件判断的语法感到困惑,特别是对逻辑运算符-a/-o和&&/||的区别不太清楚。
提示:Shell中的条件判断主要依赖test命令(即[ ])和双方括号[[ ]],它们在功能和使用上有重要区别。
我们先来看一个最简单的if语句结构:
bash复制if [ condition ]; then
commands
fi
这里的方括号[ ]实际上是test命令的另一种写法。当我们需要检查多个条件时,就需要用到逻辑运算符来组合多个测试条件。
-a表示逻辑与(AND),-o表示逻辑或(OR),它们是test命令内置的运算符,只能在单方括号[ ]中使用。
bash复制# 检查文件是否存在且可读
if [ -f "file.txt" -a -r "file.txt" ]; then
echo "文件存在且可读"
fi
# 检查变量值在某个范围内
if [ "$var" -gt 10 -o "$var" -lt 5 ]; then
echo "变量值大于10或小于5"
fi
-a/-o运算符的特点是:
&&和||是Shell的列表运算符,用于连接多个命令,根据前一个命令的退出状态决定是否执行下一个命令。
bash复制# 使用&&的示例
[ -f "file.txt" ] && echo "文件存在"
# 使用||的示例
[ -f "file.txt" ] || { echo "文件不存在"; exit 1; }
&&和||的特点是:
在较新的Bash版本中,可以使用双方括号[[ ]]来进行条件测试,它支持更自然的逻辑运算符写法:
bash复制if [[ $var > 10 && $var < 20 ]]; then
echo "变量值在10到20之间"
fi
在[[ ]]中:
bash复制# 检查文件是否存在且可写
if [ -f "/path/to/file" -a -w "/path/to/file" ]; then
echo "文件存在且可写"
fi
# 使用&&的等价写法
if [ -f "/path/to/file" ] && [ -w "/path/to/file" ]; then
echo "文件存在且可写"
fi
bash复制# 检查数值是否在10到20之间
if [ "$num" -ge 10 -a "$num" -le 20 ]; then
echo "数值在范围内"
fi
# 使用[[ ]]的更直观写法
if [[ $num -ge 10 && $num -le 20 ]]; then
echo "数值在范围内"
fi
bash复制# 检查字符串是否是特定值之一
if [ "$str" = "yes" -o "$str" = "y" -o "$str" = "YES" ]; then
echo "用户选择了是"
fi
# 使用[[ ]]和正则表达式的更简洁写法
if [[ "$str" =~ ^(yes|y|YES)$ ]]; then
echo "用户选择了是"
fi
语法位置不同:
求值顺序不同:
兼容性差异:
可读性差异:
注意:在[ ]中使用&&/||作为逻辑运算符是错误的语法,会导致错误。
逻辑运算符的优先级有时会导致意外结果。例如:
bash复制if [ "$a" = "1" -o "$b" = "2" -a "$c" = "3" ]; then
...
fi
这个表达式的求值顺序可能不符合预期。正确的做法是使用括号明确优先级:
bash复制if [ "$a" = "1" -o \( "$b" = "2" -a "$c" = "3" \) ]; then
...
fi
或者在[[ ]]中使用更清晰的写法:
bash复制if [[ "$a" = "1" || ( "$b" = "2" && "$c" = "3" ) ]]; then
...
fi
在使用[ ]时,如果变量可能为空,必须加引号:
bash复制# 错误的写法,如果$var为空会报错
if [ $var = "value" ]; then
...
fi
# 正确的写法
if [ "$var" = "value" ]; then
...
fi
而[[ ]]不需要这个预防措施:
bash复制# 在[[ ]]中不需要引号
if [[ $var = "value" ]]; then
...
fi
比较数字和字符串需要使用不同的运算符:
bash复制# 字符串比较
if [ "$str1" = "$str2" ]; then
...
fi
# 数值比较
if [ "$num1" -eq "$num2" ]; then
...
fi
在[[ ]]中,>和<用于字符串比较,数值比较仍然需要使用-eq、-ne等。
在大多数情况下,[ ]和[[ ]]的性能差异可以忽略不计。但在循环中执行数百万次时:
[[ ]]支持强大的模式匹配功能:
bash复制# 通配符匹配
if [[ "$file" == *.txt ]]; then
echo "文本文件"
fi
# 正则表达式匹配
if [[ "$str" =~ ^[0-9]+$ ]]; then
echo "纯数字"
fi
可以将if语句与&&/||结合使用,写出更简洁的代码:
bash复制# 检查并创建目录
[ -d "/tmp/mydir" ] || mkdir -p "/tmp/mydir"
# 检查命令是否成功执行
command && echo "成功" || echo "失败"
对于复杂的多条件判断,合理的格式化可以提高可读性:
bash复制if [[ $condition1 ]] \
&& [[ $condition2 ]] \
|| { [[ $condition3 ]] && [[ $condition4 ]]; }
then
commands
fi
或者:
bash复制if [[ ( $day = "Saturday" || $day = "Sunday" ) \
&& $temperature -gt 20 ]]
then
echo "适合户外活动"
fi
bash复制#!/bin/bash
backup_dir="/backups"
source_dir="/data"
min_free_space=1024 # 1GB
if [[ ! -d "$backup_dir" ]]; then
mkdir -p "$backup_dir" || { echo "无法创建备份目录"; exit 1; }
fi
if [[ -d "$source_dir" ]] \
&& [[ $(df -P "$backup_dir" | awk 'NR==2 {print $4}') -gt $min_free_space ]]
then
tar -czf "$backup_dir/data_$(date +%Y%m%d).tar.gz" "$source_dir"
else
echo "备份条件不满足:检查源目录是否存在或磁盘空间是否充足"
exit 1
fi
bash复制#!/bin/bash
read -p "请输入y/n确认操作: " answer
if [[ "$answer" =~ ^[Yy]([Ee][Ss])?$ ]]; then
echo "用户确认"
elif [[ "$answer" =~ ^[Nn][Oo]?$ ]]; then
echo "用户取消"
else
echo "无效输入"
exit 1
fi
bash复制#!/bin/bash
service_name="nginx"
if systemctl is-active --quiet "$service_name"; then
echo "$service_name 正在运行"
if systemctl is-enabled --quiet "$service_name"; then
echo "且设置为开机启动"
else
echo "但没有设置为开机启动"
fi
else
echo "$service_name 没有运行"
fi
虽然[[ ]]提供了更强大的功能,但它不是POSIX标准的一部分。如果脚本需要在各种Shell环境中运行,应该注意:
对于必须使用[[ ]]的情况,可以添加兼容性检查:
bash复制if [ -z "$BASH" ]; then
echo "本脚本需要Bash环境" >&2
exit 1
fi
为了确保条件判断按预期工作,应该:
一个简单的测试框架示例:
bash复制#!/bin/bash
test_condition() {
local condition="$1"
local expected="$2"
if eval "$condition"; then
result="true"
else
result="false"
fi
if [[ "$result" == "$expected" ]]; then
echo "PASS: $condition"
else
echo "FAIL: $condition (expected $expected, got $result)"
fi
}
# 测试用例
test_condition "[ 1 -eq 1 ]" "true"
test_condition "[ 1 -eq 2 ]" "false"
test_condition "[ 1 -eq 1 -a 2 -eq 2 ]" "true"
test_condition "[ 1 -eq 1 -o 1 -eq 2 ]" "true"
在实际Shell脚本开发中,我个人的经验是:
一个特别有用的技巧是使用函数封装复杂的条件判断:
bash复制is_valid_input() {
[[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] \
&& [[ ${#1} -ge 4 ]] \
&& [[ ${#1} -le 20 ]]
}
if is_valid_input "$username"; then
echo "用户名有效"
else
echo "无效用户名"
fi
最后,记住Shell脚本中的条件判断虽然强大,但过度复杂的逻辑可能意味着应该考虑用其他语言(如Python)来实现。当条件判断变得难以理解和维护时,就是考虑重构的好时机。