在Shell脚本编程中,if语句的条件判断是控制程序流程的核心结构。当我们需要同时满足多个条件时,就需要理解Shell中的逻辑运算符工作机制。与许多高级编程语言不同,Shell脚本的条件判断有其独特的语法和实现方式。
Shell中实现多条件判断主要依赖两种逻辑运算符:
-a 或 && 表示逻辑与(AND)-o 或 || 表示逻辑或(OR)其中-a和-o是test命令(即[ ])内部使用的运算符,而&&和||是Shell本身的逻辑控制运算符。这两种形式在功能上等效,但在使用场景和执行机制上有重要区别。
关键区别:
-a/-o必须用在单个[ ]或[[ ]]测试表达式内部,而&&/||用于连接多个独立的[ ]测试命令或其它命令。
Shell中标准的条件测试有三种形式:
test expression[ expression ](注意方括号内必须有空格)[[ expression ]](bash/zsh等现代Shell支持)这三种形式本质上是等价的,[实际上是test命令的另一种写法。而[[ ]]是bash等Shell的扩展语法,支持更多功能且更安全。
这是最基础的多条件判断方式,适用于简单的条件组合:
bash复制if [ $age -gt 18 -a $age -lt 60 ]; then
echo "符合工作年龄要求"
fi
这个例子检查年龄是否在18到60之间。-a表示两个条件必须同时满足。
注意事项:
-a和-o必须用在同一个[ ]表达式内部-gt、-eq等)=或==,数字比较要用-eq、-gt等更灵活的方式是使用Shell的逻辑运算符连接多个独立的[ ]测试命令:
bash复制if [ $gender = "male" ] && [ $age -ge 22 ]; then
echo "符合男性结婚年龄要求"
fi
这种写法的优势:
现代Shell(如bash)支持[[ ]]条件表达式,它比传统的[ ]更强大:
bash复制if [[ $file == *.txt && -r $file && ! -d $file ]]; then
echo "这是一个可读的文本文件"
fi
[[ ]]的优势包括:
==和!=)&&和||运算符(不需要用-a和-o)=~)对于非常复杂的多条件判断,可以使用嵌套if结构:
bash复制if [ -f "$file" ]; then
if [ -r "$file" ]; then
if [ -s "$file" ]; then
echo "文件存在、可读且非空"
fi
fi
fi
虽然这种写法略显冗长,但在以下场景很有价值:
bash复制#!/bin/bash
file="/var/log/app.log"
if [[ -f "$file" && -r "$file" && -w "$file" ]]; then
echo "日志文件存在且可读写"
if grep -q "ERROR" "$file" && [ $(wc -l < "$file") -gt 1000 ]; then
echo "发现错误日志且文件较大,建议归档处理"
fi
elif [ ! -e "$file" ]; then
echo "警告:日志文件不存在"
else
echo "日志文件存在但权限不足"
fi
这个脚本展示了:
bash复制#!/bin/bash
read -p "请输入用户名(4-12位字母数字): " username
if [[ "$username" =~ ^[a-zA-Z0-9]{4,12}$ ]] &&
! grep -q "^$username:" /etc/passwd; then
echo "用户名有效且未被占用"
else
echo "用户名无效或已被占用" >&2
exit 1
fi
这个例子演示了:
bash复制#!/bin/bash
cpu_threshold=80
mem_threshold=90
disk_threshold=85
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}')
mem_usage=$(free | awk '/Mem/{printf("%.0f"), $3/$2*100}')
disk_usage=$(df -h / | awk 'NR==2{print $5}' | tr -d '%')
if (( $(echo "$cpu_usage > $cpu_threshold" | bc -l) )) &&
(( $(echo "$mem_usage > $mem_threshold" | bc -l) )); then
echo "警报:CPU和内存使用率同时超标!" >&2
echo "CPU: ${cpu_usage}% 内存: ${mem_usage}%" >&2
fi
if [ "$disk_usage" -ge "$disk_threshold" ] ||
[ ! -f "/tmp/emergency_space_created" ]; then
echo "磁盘空间不足或紧急文件未创建" >&2
fi
这个案例展示了:
Shell中的&&和||具有短路求值特性,这可以用于优化脚本性能:
bash复制# 只有当前一个命令成功才会执行后一个命令
[ -f "$config" ] && source "$config"
# 只有当前一个命令失败才会执行后一个命令
[ -x "$cmd" ] || { echo "命令不可执行"; exit 1; }
短路求值的实用场景:
正确处理变量引用是避免条件判断错误的关键:
bash复制# 危险:变量未加引号,空值或含空格会出问题
if [ -f $file ]; then ...
# 安全:始终用引号包裹变量
if [ -f "$file" ]; then ...
# 更安全:使用[[ ]]处理变量
if [[ -f $file ]]; then ... # [[ ]]内变量即使含空格也安全
常见变量相关错误:
使用括号可以明确指定条件评估的优先级:
bash复制if [ \( "$count" -gt 0 -a "$count" -lt 100 \) -o -f "$flag_file" ]; then
echo "条件满足"
fi
# 等效的[[ ]]版本(更清晰)
if [[ ($count -gt 0 && $count -lt 100) || -f $flag_file ]]; then
echo "条件满足"
fi
注意:在
[ ]中使用括号需要转义((和)),而在[[ ]]中可以直接使用
对于复杂的条件判断,应考虑性能影响:
优化示例:
bash复制# 不佳:每次条件判断都执行一次grep
if grep -q "error" "$log" && [ $(wc -l < "$log") -gt 100 ]; then...
# 优化:存储结果避免重复执行
has_error=$(grep -q "error" "$log" && echo true || echo false)
line_count=$(wc -l < "$log")
if $has_error && [ $line_count -gt 100 ]; then...
[[ ]]是bash/zsh等扩展功能,POSIX shell(如dash)不支持==模式匹配是bash扩展,POSIX shell只能用==~正则表达式匹配仅bash支持兼容性写法示例:
bash复制# 兼容POSIX的写法
if [ "$OSTYPE" = "linux-gnu" ] || [ "$OSTYPE" = "darwin" ]; then...
# Bash特有的写法
if [[ "$OSTYPE" == @(linux-gnu|darwin)* ]]; then...
不同Shell中数值比较的差异:
| 比较方式 | 示例 | 适用Shell |
|---|---|---|
-eq, -ne等 |
[ "$a" -eq "$b" ] |
所有Shell |
(( ))算术比较 |
(( a > b )) |
bash/ksh/zsh |
$(( ))扩展 |
[ $((a > b)) -eq 1 ] |
POSIX兼容 |
推荐做法:
[ ]和-eq等操作符(( ))$(( ))编写可移植Shell脚本的建议:
#!/bin/sh或#!/bin/bash[[ ]]和(( ))等明确标记可移植性处理示例:
bash复制#!/bin/sh
# 检测是否支持[[ ]]
if (eval "[[ 1 == 1 ]]") 2>/dev/null; then
USE_DOUBLE_BRACKET=true
else
USE_DOUBLE_BRACKET=false
fi
# 根据检测结果使用适当的语法
if $USE_DOUBLE_BRACKET; then
if [[ "$var" == *.txt ]]; then...
else
case "$var" in
*.txt) ... ;;
esac
fi
对于特别复杂的条件判断,可以分解为多个步骤:
bash复制# 初始条件
has_permission=false
is_valid_time=false
resource_available=false
# 分解条件检查
[ -r "$file" -a -w "$file" ] && has_permission=true
[ "$(date +%H)" -ge 9 -a "$(date +%H)" -lt 18 ] && is_valid_time=true
[ -f "/tmp/resource.lock" ] || resource_available=true
# 组合判断
if $has_permission && $is_valid_time && $resource_available; then
echo "所有条件满足,开始处理"
fi
这种模式的优点:
对于需要检查大量条件的场景,可以使用表驱动方法:
bash复制#!/bin/bash
# 定义条件检查表
declare -A conditions=(
["文件存在"]="-f /path/to/file"
["服务运行"]="systemctl is-active --quiet nginx"
["端口监听"]="netstat -tuln | grep -q ':80 '"
["内存足够"]="[ $(free -m | awk '/Mem/{print $7}') -gt 512 ]"
)
# 执行条件检查
all_ok=true
for desc in "${!conditions[@]}"; do
if ! eval "${conditions[$desc]}"; then
echo "条件不满足: $desc"
all_ok=false
fi
done
# 最终判断
$all_ok && echo "所有条件满足" || echo "有未满足的条件"
这种模式的适用场景:
对于有复杂状态转换的逻辑,可以实现简单的状态机:
bash复制#!/bin/bash
STATE="INIT"
while true; do
case "$STATE" in
INIT)
if [ -f config.ini ]; then
echo "找到配置文件"
STATE="CONFIG_LOADED"
else
echo "缺少配置文件"
STATE="ERROR"
fi
;;
CONFIG_LOADED)
if check_dependencies; then
echo "依赖满足"
STATE="DEPS_OK"
else
echo "依赖缺失"
STATE="ERROR"
fi
;;
DEPS_OK)
if [ "$(id -u)" -eq 0 ]; then
echo "以root权限运行"
STATE="READY"
else
echo "需要root权限"
STATE="ERROR"
fi
;;
READY)
echo "所有条件满足,开始主流程"
break
;;
ERROR)
echo "流程错误,退出"
exit 1
;;
esac
done
状态机模式的优点:
在调试复杂条件时,可以添加详细的调试输出:
bash复制#!/bin/bash
set -x # 开启命令追踪
debug() { [ "$DEBUG" = "true" ] && echo "DEBUG: $*" >&2; }
file="/path/to/test"
debug "开始文件检查"
if [ -f "$file" ]; then
debug "文件存在"
if [ -r "$file" ]; then
debug "文件可读"
if [ -s "$file" ]; then
debug "文件非空"
echo "文件检查通过"
else
debug "文件为空"
fi
else
debug "文件不可读"
fi
else
debug "文件不存在"
fi
set +x # 关闭命令追踪
调试技巧:
set -x和set +x跟踪命令执行2>&1重定向调试输出到日志文件为复杂条件编写测试用例:
bash复制#!/bin/bash
# 测试函数:检查两个数字是否在范围内
test_in_range() {
local num=$1 min=$2 max=$3
[ "$num" -ge "$min" ] && [ "$num" -le "$max" ]
}
# 测试用例
run_tests() {
# 测试1:数字在范围内
test_in_range 5 1 10 && echo "测试1通过" || echo "测试1失败"
# 测试2:数字等于下限
test_in_range 1 1 10 && echo "测试2通过" || echo "测试2失败"
# 测试3:数字等于上限
test_in_range 10 1 10 && echo "测试3通过" || echo "测试3失败"
# 测试4:数字低于下限
test_in_range 0 1 10 && echo "测试4失败" || echo "测试4通过"
# 测试5:数字高于上限
test_in_range 11 1 10 && echo "测试5失败" || echo "测试5通过"
}
run_tests
测试最佳实践:
以下是多条件判断中的典型错误及解决方法:
| 错误示例 | 问题分析 | 正确写法 |
|---|---|---|
if [ $x = 1 && $y = 2 ]; then |
&&不能用在[ ]内部 |
if [ "$x" = 1 ] && [ "$y" = 2 ]; then |
if [ -f "*.log" ]; then |
通配符在[ ]中不会展开 |
if [[ -f *.log ]]; then 或 for f in *.log; do... |
if [ "$result" = "true" ]; then |
命令返回0表示成功,非字符串"true" | if command; then 或 if [ $? -eq 0 ]; then |
if [ "$count" > 10 ]; then |
>是重定向,不是比较 |
if [ "$count" -gt 10 ]; then |
if [ -n $var ]; then |
变量未引用可能导致语法错误 | if [ -n "$var" ]; then |
错误预防建议: