1. Shell条件判断基础与常见误区
在Shell脚本编程中,条件判断是最基础也最容易出错的部分。很多初学者在编写if语句时,常常混淆各种条件表达式的用法,导致脚本行为与预期不符。我们先来看一个典型的错误示例:
bash复制if [ $var1 == "test" && $var2 -lt 10 ]; then # 这种写法是错误的!
echo "条件满足"
fi
这个例子中使用了&&逻辑运算符,但在[]测试表达式中这种写法会导致语法错误。正确的写法应该是:
bash复制if [ "$var1" = "test" ] && [ "$var2" -lt 10 ]; then
echo "条件满足"
fi
1.1 测试命令的三种形式
Shell中实现条件判断主要有三种形式:
-
test命令:最原始的测试命令
bash复制test "$var" = "value" -
方括号[]:test命令的语法糖
bash复制[ "$var" = "value" ] -
双方括号[[]]:bash的扩展测试命令
bash复制[[ $var == "value" ]]
重要提示:在[]和test中,=用于字符串比较,-eq用于数字比较;而在[[]]中,==和=都可以用于字符串比较,但==更符合编程习惯。
1.2 字符串比较的坑
字符串比较是Shell脚本中最容易出问题的地方之一。看看下面这个例子:
bash复制var=""
if [ $var = "test" ]; then # 这里会报错:[: =: unary operator expected
echo "变量等于test"
fi
当变量为空时,上面的写法会导致语法错误。正确的做法是:
bash复制if [ "$var" = "test" ]; then # 总是用引号包裹变量
echo "变量等于test"
fi
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多条件判断:-a/-o与&&/||的区别
2.1 -a和-o的使用
-a表示逻辑与(AND),-o表示逻辑或(OR),它们只能在[]或test命令中使用:
bash复制if [ "$var1" = "test" -a "$var2" -lt 10 ]; then
echo "var1是test且var2小于10"
fi
if [ "$var1" = "test" -o "$var2" -lt 10 ]; then
echo "var1是test或var2小于10"
fi
2.2 &&和||的使用
&&和||是Shell的逻辑操作符,可以连接多个命令:
bash复制if [ "$var1" = "test" ] && [ "$var2" -lt 10 ]; then
echo "var1是test且var2小于10"
fi
if [ "$var1" = "test" ] || [ "$var2" -lt 10 ]; then
echo "var1是test或var2小于10"
fi
2.3 关键区别对比
| 特性 | -a/-o | &&/|| |
|------------|-------------------------|-------------------------|
| 使用环境 | 只能在[]或test中使用 | 可以在任何地方使用 |
| 短路求值 | 不支持 | 支持 |
| 可读性 | 较差 | 较好 |
| 兼容性 | 所有Shell都支持 | 所有Shell都支持 |
| 推荐程度 | 不推荐 | 推荐 |
实际经验:在现代Shell脚本中,建议总是使用&&和||而不是-a和-o,因为:
- 可读性更好
- 行为更符合预期
- 避免了[]中的很多语法陷阱
3. 高级条件判断技巧
3.1 复合条件判断
当需要组合多个条件时,使用&&和||可以构建更复杂的逻辑:
bash复制if [ -f "$file" ] && { [ "$count" -gt 10 ] || [ "$mode" = "force" ]; }; then
echo "文件存在且(计数大于10或模式为force)"
fi
注意这里使用了花括号{}来分组条件,避免歧义。
3.2 使用[[]]的高级特性
[[]]是bash的扩展测试命令,提供了更多功能:
bash复制if [[ $var == t* && $var != test ]]; then # 支持通配符匹配
echo "变量以t开头但不是test"
fi
if [[ "$str1" > "$str2" ]]; then # 支持字符串字典序比较
echo "str1在字典序中大于str2"
fi
3.3 正则表达式匹配
在[[]]中还可以使用正则表达式:
bash复制if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "有效的电子邮件地址"
fi
4. 实战案例与常见问题
4.1 文件检查综合示例
bash复制#!/bin/bash
file="$1"
mode="$2"
if [[ ! -e "$file" ]]; then
echo "错误:文件不存在" >&2
exit 1
elif [[ -d "$file" && "$mode" != "dir" ]]; then
echo "错误:这是一个目录,但模式不是dir" >&2
exit 1
elif [[ -f "$file" && -r "$file" && -s "$file" ]]; then
echo "文件存在、可读且不为空"
# 处理文件内容
else
echo "文件不符合要求"
fi
4.2 数字比较的注意事项
Shell中的数字比较有一些特殊之处:
bash复制a="10"
b="20"
if [ "$a" -lt "$b" ]; then # 正确方式:使用-lt而不是<
echo "a小于b"
fi
# 浮点数比较需要借助其他工具
if (( $(echo "$a < $b" | bc -l) )); then
echo "a小于b(支持浮点数)"
fi
4.3 条件判断中的常见错误
-
忘记引号:
bash复制if [ $var = "test" ]; then # 当var为空时会出错 -
混淆字符串和数字比较:
bash复制if [ "10" -eq "10.0" ]; then # 语法错误:-eq用于整数 -
在[]中使用&&:
bash复制if [ $a -gt 10 && $b -lt 20 ]; then # 错误,应该用-a或分开的[] -
测试命令后的分号:
bash复制if [ -f "$file"]; then # 错误,]前需要空格
5. 性能考量与最佳实践
5.1 条件判断的性能差异
在性能敏感的脚本中,条件判断的实现方式会影响执行速度:
bash复制# 测试10000次[]和[[]]的性能差异
time for i in {1..10000}; do [ "$i" -lt 10000 ]; done
time for i in {1..10000}; do [[ "$i" -lt 10000 ]]; done
通常[[]]比[]稍快,但在大多数情况下差异可以忽略不计。
5.2 条件判断的最佳实践
-
总是引用变量:
bash复制if [ -f "$file" ]; then... -
使用[[ ]]代替[ ]:
bash复制if [[ $var == pattern ]]; then... -
对整数比较使用(( )):
bash复制if (( count > 10 )); then... -
避免过度复杂的条件:
bash复制# 不好 if [[ $a && ($b || $c) && !$d ]]; then... # 更好 if [[ $a ]]; then if [[ $b || $c ]]; then if [[ ! $d ]]; then... -
添加有意义的注释:
bash复制# 检查用户是否是管理员或处于调试模式 if [[ $user == "admin" ]] || [[ $debug == "true" ]]; then...
6. 跨平台兼容性考虑
6.1 不同Shell的差异
不同的Shell对条件判断的支持有所不同:
| 特性 | Bash | Dash | Zsh | Ksh |
|---|---|---|---|---|
[[]] |
支持 | 不支持 | 支持 | 支持 |
-a/-o |
支持 | 支持 | 支持 | 支持 |
| `&&/ | ` | 支持 | 支持 | |
| 正则匹配 | 支持 | 不支持 | 支持 | 支持 |
6.2 编写可移植脚本的建议
- 如果只需要基本功能,使用
[]和-a/-o - 如果需要高级功能,在脚本开头指定解释器:
bash复制#!/bin/bash - 对于可能为空的变量,总是测试其存在性:
bash复制if [ "${var:-}" = "value" ]; then... - 避免使用bash特有的功能如
[[ ]]和=~如果脚本需要在多种Shell中运行
7. 调试技巧与工具
7.1 调试模式运行脚本
使用-x选项可以显示脚本执行的每一行:
bash复制bash -x script.sh
7.2 打印变量值
在条件判断前后打印变量值有助于调试:
bash复制echo "Debug: var1=$var1, var2=$var2" >&2
if [[ "$var1" -gt "$var2" ]]; then
echo "var1大于var2"
fi
7.3 使用set -euo pipefail
在脚本开头设置这些选项可以更严格地捕获错误:
bash复制#!/bin/bash
set -euo pipefail
# -e: 命令失败时立即退出
# -u: 使用未定义变量时报错
# -o pipefail: 管道中任意命令失败则整个管道失败
8. 实际应用案例
8.1 系统监控脚本
bash复制#!/bin/bash
# 检查CPU负载
cpu_load=$(uptime | awk '{print $NF}')
if [[ $(echo "$cpu_load > 2.0" | bc) -eq 1 ]]; then
echo "警告:CPU负载过高 - $cpu_load" >&2
fi
# 检查磁盘空间
disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [[ $disk_usage -gt 90 ]]; then
echo "警告:磁盘使用率超过90% - 当前${disk_usage}%" >&2
fi
# 检查内存使用
free_mem=$(free -m | awk '/Mem:/ {print $7}')
if [[ $free_mem -lt 100 ]]; then
echo "警告:可用内存不足100MB - 当前${free_mem}MB" >&2
fi
8.2 用户输入验证
bash复制#!/bin/bash
read -p "请输入年龄: " age
if [[ ! "$age" =~ ^[0-9]+$ ]]; then
echo "错误:年龄必须是数字" >&2
exit 1
elif [[ "$age" -lt 18 ]]; then
echo "抱歉,您必须年满18岁"
elif [[ "$age" -ge 18 && "$age" -le 65 ]]; then
echo "欢迎访问"
else
echo "欢迎,长者用户"
fi
8.3 服务状态检查
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 "$service_name 已设置为开机启动"
else
echo "警告:$service_name 未设置为开机启动" >&2
fi
else
echo "错误:$service_name 未运行" >&2
if [[ -x "$(command -v docker)" ]] && docker ps | grep -q "$service_name"; then
echo "提示:$service_name 似乎在Docker容器中运行"
fi
fi
9. 性能优化技巧
9.1 减少子进程调用
条件判断中避免不必要的子进程调用:
bash复制# 不好:使用了子进程
if [ $(who | wc -l) -gt 3 ]; then...
# 更好:使用内置功能
if (( $(who | wc -l) > 3 )); then...
# 最好:完全避免子进程
users=()
while IFS= read -r line; do
users+=("$line")
done < <(who)
if (( ${#users[@]} > 3 )); then...
9.2 使用case语句替代复杂if
当有多个条件分支时,case语句通常更高效:
bash复制# 使用if
if [[ "$response" == "Y" || "$response" == "y" ]]; then
echo "是"
elif [[ "$response" == "N" || "$response" == "n" ]]; then
echo "否"
else
echo "无效输入"
fi
# 使用case(更高效)
case "$response" in
[Yy]) echo "是" ;;
[Nn]) echo "否" ;;
*) echo "无效输入" ;;
esac
9.3 提前终止不必要的检查
利用短路求值特性,将最可能失败的条件放在前面:
bash复制# 不好:先检查次要条件
if [[ -f "$log_file" && "$debug" == "true" ]]; then...
# 更好:先检查主要条件
if [[ "$debug" == "true" && -f "$log_file" ]]; then...
10. 安全注意事项
10.1 防止条件判断中的注入攻击
bash复制# 不安全:用户输入可能包含恶意命令
read -p "输入文件名: " filename
if [ -f "$filename" ]; then... # 如果用户输入"malicious.txt; rm -rf /"
# 更安全:验证输入格式
if [[ "$filename" =~ ^[a-zA-Z0-9_.-]+$ ]] && [ -f "./$filename" ]; then...
10.2 权限检查
在操作文件前检查权限:
bash复制file="/etc/sensitive.conf"
if [[ ! -r "$file" ]]; then
echo "错误:无法读取文件 $file" >&2
exit 1
fi
if [[ -w "$file" ]]; then
echo "警告:你有写入 $file 的权限" >&2
fi
10.3 使用不可预测的临时文件
bash复制# 不安全:固定的临时文件名可能导致竞争条件
tempfile="/tmp/mytempfile"
if [ -f "$tempfile" ]; then...
# 更安全:使用随机文件名
tempfile=$(mktemp /tmp/myscript.XXXXXX)
if [ -f "$tempfile" ]; then...
11. 特殊条件判断技巧
11.1 检查命令是否存在
bash复制if command -v git &>/dev/null; then
echo "git已安装"
else
echo "git未安装" >&2
exit 1
fi
11.2 检查数组是否包含元素
bash复制fruits=("apple" "banana" "orange")
search="banana"
if [[ " ${fruits[@]} " =~ " $search " ]]; then
echo "找到 $search"
fi
11.3 检查变量是否设置
bash复制# 检查变量是否设置且非空
if [[ -n "${var:-}" ]]; then...
# 检查变量是否未设置或为空
if [[ -z "${var:-}" ]]; then...
12. 现代Shell脚本实践
12.1 使用ShellCheck进行静态分析
ShellCheck是一个Shell脚本静态分析工具,可以检测常见错误:
bash复制# 安装ShellCheck
# Ubuntu/Debian:
sudo apt install shellcheck
# 使用ShellCheck检查脚本
shellcheck script.sh
12.2 编写可测试的条件判断
将复杂条件提取为函数,便于测试:
bash复制is_valid_user() {
local username=$1
[[ "$username" =~ ^[a-z_][a-z0-9_]{0,31}$ ]] && id "$username" &>/dev/null
}
if is_valid_user "$1"; then
echo "有效用户"
fi
12.3 使用关联数组管理复杂条件
bash复制declare -A checks
checks["disk"]=90
checks["memory"]=80
checks["cpu"]=70
for item in "${!checks[@]}"; do
threshold="${checks[$item]}"
case "$item" in
disk) current=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') ;;
memory) current=$(free | awk '/Mem:/ {printf("%.0f", $3/$2*100)}') ;;
cpu) current=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') ;;
esac
if (( current > threshold )); then
echo "警告:$item 使用率 ${current}% 超过阈值 ${threshold}%" >&2
fi
done
13. 条件判断中的性能测量
13.1 测量条件判断执行时间
bash复制# 测量[ ]的性能
time for i in {1..1000}; do [ "$i" -lt 1000 ]; done
# 测量[[ ]]的性能
time for i in {1..1000}; do [[ "$i" -lt 1000 ]]; done
# 测量(( ))的性能
time for i in {1..1000}; do (( i < 1000 )); done
13.2 不同条件写法的性能比较
bash复制# 方法1:使用多个if
time {
for i in {1..1000}; do
if [ "$i" -gt 100 ]; then
if [ "$i" -lt 900 ]; then
:
fi
fi
done
}
# 方法2:使用-a连接
time {
for i in {1..1000}; do
if [ "$i" -gt 100 -a "$i" -lt 900 ]; then
:
fi
done
}
# 方法3:使用&&连接
time {
for i in {1..1000}; do
if [ "$i" -gt 100 ] && [ "$i" -lt 900 ]; then
:
fi
done
}
14. 条件判断在函数中的使用
14.1 函数参数验证
bash复制process_file() {
local file="$1"
local mode="${2:-default}"
if [[ ! -f "$file" ]]; then
echo "错误:文件 $file 不存在" >&2
return 1
fi
if [[ "$mode" != "default" && "$mode" != "verbose" ]]; then
echo "错误:无效模式 $mode" >&2
return 1
fi
# 处理文件
echo "处理文件 $file (模式: $mode)"
}
14.2 函数返回状态检查
bash复制is_even() {
local num="$1"
(( num % 2 == 0 ))
}
if is_even 42; then
echo "42是偶数"
fi
14.3 函数中的复杂条件
bash复制can_install_package() {
local pkg="$1"
local version="${2:-*}"
# 检查是否已安装
if dpkg -l "$pkg" | grep -q "^ii $pkg"; then
echo "$pkg 已安装" >&2
return 1
fi
# 检查仓库中是否有该版本
if ! apt-cache show "$pkg=$version" &>/dev/null; then
echo "$pkg 版本 $version 不可用" >&2
return 1
fi
# 检查依赖是否满足
if ! apt-get install -s "$pkg=$version" >/dev/null; then
echo "$pkg 依赖不满足" >&2
return 1
fi
return 0
}
if can_install_package "nginx" "1.18*"; then
sudo apt-get install -y nginx=1.18*
fi
15. 条件判断与错误处理
15.1 优雅的错误处理
bash复制if ! cd "$dir"; then
echo "无法进入目录 $dir" >&2
exit 1
fi
if ! command -v jq &>/dev/null; then
echo "错误:jq未安装,正在尝试安装..." >&2
if ! sudo apt-get install -y jq; then
echo "安装jq失败" >&2
exit 1
fi
fi
15.2 使用trap处理错误
bash复制cleanup() {
echo "清理临时文件..."
rm -f "$temp_file"
}
trap cleanup EXIT ERR
temp_file=$(mktemp)
if ! some_command > "$temp_file"; then
echo "命令执行失败" >&2
exit 1
fi
15.3 记录详细的错误信息
bash复制log_error() {
local message="$1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 错误: $message" >> "$log_file"
}
if [[ ! -d "$backup_dir" ]]; then
log_error "备份目录 $backup_dir 不存在"
if ! mkdir -p "$backup_dir"; then
log_error "无法创建备份目录 $backup_dir"
exit 1
fi
fi
16. 条件判断与循环结合
16.1 while循环中的条件
bash复制count=0
while [[ $count -lt 5 ]]; do
echo "计数: $count"
((count++))
done
16.2 for循环中的条件判断
bash复制for file in *; do
if [[ -d "$file" ]]; then
echo "目录: $file"
elif [[ -x "$file" ]]; then
echo "可执行文件: $file"
else
echo "普通文件: $file"
fi
done
16.3 until循环的使用
bash复制attempt=0
max_attempts=5
until [[ -f "$lockfile" || $attempt -ge $max_attempts ]]; do
((attempt++))
echo "尝试 $attempt/$max_attempts"
if touch "$lockfile" 2>/dev/null; then
echo "成功获取锁"
break
fi
sleep 1
done
17. 条件判断与数组操作
17.1 检查数组是否为空
bash复制files=(*)
if [[ ${#files[@]} -eq 0 ]]; then
echo "当前目录为空"
else
echo "找到 ${#files[@]} 个文件"
fi
17.2 数组元素的条件过滤
bash复制numbers=(1 2 3 4 5 6 7 8 9 10)
even_numbers=()
for num in "${numbers[@]}"; do
if (( num % 2 == 0 )); then
even_numbers+=("$num")
fi
done
echo "偶数: ${even_numbers[@]}"
17.3 关联数组的条件判断
bash复制declare -A users=(
["alice"]="admin"
["bob"]="user"
["eve"]="guest"
)
if [[ -n "${users[$1]:-}" ]]; then
echo "用户 $1 的角色是 ${users[$1]}"
else
echo "未知用户 $1"
fi
18. 条件判断与进程控制
18.1 检查进程是否运行
bash复制if pgrep -x "nginx" >/dev/null; then
echo "nginx正在运行"
else
echo "nginx未运行"
fi
18.2 等待条件满足
bash复制timeout=60
interval=5
elapsed=0
while [[ $elapsed -lt $timeout ]]; do
if systemctl is-active --quiet "mysql"; then
echo "MySQL已启动"
break
fi
sleep $interval
((elapsed += interval))
done
if [[ $elapsed -ge $timeout ]]; then
echo "等待MySQL启动超时" >&2
exit 1
fi
18.3 并行任务的条件控制
bash复制max_jobs=4
for item in {1..10}; do
if [[ $(jobs -r | wc -l) -ge $max_jobs ]]; then
wait -n
fi
(
# 处理任务
echo "处理项目 $item"
sleep $((RANDOM % 3 + 1))
) &
done
wait
19. 条件判断与信号处理
19.1 捕获信号并设置标志
bash复制interrupted=false
trap 'interrupted=true' INT TERM
for i in {1..10}; do
if [[ "$interrupted" == true ]]; then
echo "检测到中断,正在退出..."
break
fi
echo "处理项目 $i"
sleep 1
done
19.2 超时处理
bash复制timeout=5
cmd="sleep 10"
if ! "$cmd" & pid=$!; then
echo "启动命令失败" >&2
exit 1
fi
(
sleep "$timeout"
if kill -0 "$pid" 2>/dev/null; then
echo "命令超时,正在终止..." >&2
kill "$pid"
fi
) & watcher=$!
if wait "$pid" 2>/dev/null; then
echo "命令成功完成"
kill "$watcher" 2>/dev/null
else
echo "命令失败或超时" >&2
exit 1
fi
20. 条件判断与文件描述符
20.1 检查文件描述符是否可用
bash复制if { true >&3; } 2>/dev/null; then
echo "文件描述符3可用"
else
echo "文件描述符3未打开" >&2
fi
20.2 条件重定向
bash复制log_file="output.log"
verbose=true
if [[ "$verbose" == true ]]; then
exec 3>&1 # 保存标准输出
exec 1> >(tee "$log_file") # 同时输出到文件和屏幕
else
exec 3>&1 # 保存标准输出
exec 1>"$log_file" # 只输出到文件
fi
echo "这条消息会根据verbose设置输出到不同地方"
# 恢复标准输出
exec 1>&3 3>&-
20.3 检查管道命令是否成功
bash复制if ! command1 | command2 | command3; then
echo "管道命令失败" >&2
# 检查哪个命令失败
if ! command1; then
echo "command1失败" >&2
elif ! command2; then
echo "command2失败" >&2
else
echo "command3失败" >&2
fi
fi
