第4章 条件判断
条件判断是Shell脚本实现逻辑控制的基础。本章将详细介绍Shell中的条件判断语法,包括test命令、[ ] 和 [[ ]] 的区别,以及字符串比较、数值比较和文件属性判断的各种用法,并提供大量实用的判断场景和示例。
4.1 test命令与条件语法
4.1.1 test命令简介
test 是Shell内置的条件判断命令,用于评估表达式并返回退出状态码(0表示真,1表示假):
bash
# 基本语法
test EXPRESSION
[ EXPRESSION ]
# 例如:判断数值是否相等
test 5 -eq 5
echo $? # 输出:0(真)
[ 5 -eq 5 ]
echo $? # 输出:0(真)
# 也可以使用 [[ ]] 语法(Bash扩展)
[[ 5 -eq 5 ]]
echo $? # 输出:0(真)
4.1.2 [ ] 与 [[ ]] 的区别
| 特性 | [ ] | [[ ]] |
|---|---|---|
| POSIX标准 | POSIX兼容 | Bash扩展 |
| 模式匹配 | ❌ 不支持 | ✅ 支持 |
| 字符串运算符 | = 和 != |
=、==、!= |
| 逻辑运算符 | -a、-o |
&&、` |
| 变量引号 | 必须加引号 | 不需要 |
| 边界处理 | 需要注意空变量 | 更安全 |
bash
# [ ] 示例(需要注意变量加引号)
name=""
[ $name = "test" ] # 错误:空变量会导致语法错误
[ "$name" = "test" ] # 正确:加引号
# [[ ]] 示例(更安全)
name=""
[[ $name = "test" ]] # 正确:不需要加引号
[[ $name == t* ]] # 正确:支持模式匹配
# [[ ]] 支持正则表达式匹配
email="user@example.com"
[[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] && echo "邮箱格式正确"
建议 :在Bash脚本中优先使用 [[ ]],在需要兼容POSIX sh时使用 [ ]。
4.1.3 (( )) 用于数学比较
Bash还提供了 (( )) 用于数学比较,特别适合数值比较:
bash
# 使用 (( )) 进行数学比较
a=10
b=20
(( a < b )) && echo "a小于b" # 输出:a小于b
(( a == 10 )) && echo "a等于10"
# (( )) 支持数学表达式
(( a + 5 > b )) && echo "a+5大于b"
4.2 字符串比较
4.2.1 基本字符串比较运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
= |
字符串相等 | [ "$a" = "$b" ] |
!= |
字符串不相等 | [ "$a" != "$b" ] |
-z |
字符串为空(长度为0) | [ -z "$a" ] |
-n |
字符串非空(长度大于0) | [ -n "$a" ] |
bash
# 基本字符串比较
str1="hello"
str2="hello"
str3="world"
str4=""
# 相等判断
[ "$str1" = "$str2" ] && echo "str1等于str2" # 输出:str1等于str2
[ "$str1" != "$str3" ] && echo "str1不等于str3" # 输出:str1不等于str3
# 空值判断
[ -z "$str4" ] && echo "str4为空" # 输出:str4为空
[ -n "$str1" ] && echo "str1非空" # 输出:str1非空
# 注意:未定义的变量也会被当作空值
unset undefined_var
[ -z "$undefined_var" ] && echo "undefined_var为空" # 输出:undefined_var为空
4.2.2 字符串比较的注意事项
bash
# 正确用法:始终给变量加引号
name="Zhang San"
[ "$name" = "Zhang San" ] && echo "匹配"
# 错误用法:不加引号会导致问题
# [ $name = Zhang San ] # 错误:会被解析为多个参数
# [ $name = "Zhang San" ] # 如果name为空,会报错
# 使用 [[ ]] 更安全
name=""
[[ $name = "" ]] && echo "空字符串" # 正确,不会报错
# 变量为空时的安全检查
name=""
# 方式1:使用引号
[ "$name" = "" ] && echo "空字符串"
# 方式2:使用 -z
[ -z "$name" ] && echo "空字符串"
# 方式3:使用默认值
echo "${name:-默认值}" # 输出:默认值
4.2.3 字符串排序比较
使用 > 或 < 进行字典序比较(注意需要转义):
bash
str1="apple"
str2="banana"
# [[ ]] 中的比较
[[ "$str1" < "$str2" ]] && echo "apple字典序小于banana" # 输出
[[ "$str1" > "$str2" ]] && echo "apple字典序大于banana"
# [ ] 中需要转义
[ "$str1" \< "$str2" ] && echo "apple字典序小于banana"
# ASCII码比较
[ "a" \< "b" ] && echo "a的ASCII码小于b"
# 字符串长度比较
[ ${#str1} -lt ${#str2} ] && echo "str1比str2短"
4.2.4 字符串模式匹配
使用 [[ ]] 支持的模式匹配功能:
bash
# 通配符匹配
filename="document.txt"
[[ $filename == *.txt ]] && echo "是文本文件"
[[ $filename == doc* ]] && echo "以doc开头"
[[ $filename == *.* ]] && echo "包含点号"
# 正则表达式匹配(Bash 3.0+)
email="user@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "邮箱格式正确"
fi
# 捕获分组
str="2024-01-15"
if [[ $str =~ ^([0-9]+)-([0-9]+)-([0-9]+)$ ]]; then
echo "年: ${BASH_REMATCH[1]}"
echo "月: ${BASH_REMATCH[2]}"
echo "日: ${BASH_REMATCH[3]}"
fi
4.3 数值比较
4.3.1 常用数值比较运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
-eq |
等于(equal) | [ 5 -eq 5 ] |
-ne |
不等于(not equal) | [ 5 -ne 3 ] |
-gt |
大于(greater than) | [ 5 -gt 3 ] |
-ge |
大于或等于(greater or equal) | [ 5 -ge 5 ] |
-lt |
小于(less than) | [ 3 -lt 5 ] |
-le |
小于或等于(less or equal) | [ 3 -le 3 ] |
bash
# 基本数值比较
a=10
b=20
[ $a -eq 10 ] && echo "a等于10" # 输出:a等于10
[ $a -ne $b ] && echo "a不等于b" # 输出:a不等于b
[ $a -gt 5 ] && echo "a大于5" # 输出:a大于5
[ $a -ge 10 ] && echo "a大于等于10" # 输出:a大于等于10
[ $a -lt $b ] && echo "a小于b" # 输出:a小于b
[ $a -le 10 ] && echo "a小于等于10" # 输出:a小于等于10
4.3.2 复合条件判断
结合逻辑运算符进行复合判断:
bash
# 数值比较中的逻辑运算
# [ ] 中使用 -a(与)、-o(或)
a=15
[ $a -gt 10 -a $a -lt 20 ] && echo "a在10和20之间" # 输出
# [[ ]] 中使用 &&、||
a=15
[[ $a -gt 10 && $a -lt 20 ]] && echo "a在10和20之间" # 输出
# 否定运算
[ ! $a -eq 10 ] && echo "a不等于10" # 输出
# 复杂复合条件
age=25
city="Beijing"
[ $age -ge 18 -a $age -le 60 ] && [ "$city" = "Beijing" ] && echo "满足条件"
4.3.3 浮点数比较
test 命令不支持浮点数比较,需要借助 bc 或其他方法:
bash
# 浮点数比较(使用bc)
x=3.14
y=2.71
# 比较两个浮点数是否相等
result=$(echo "$x == $y" | bc)
[ $result -eq 1 ] && echo "相等" || echo "不相等" # 输出:不相等
# 比较大小
result=$(echo "$x > $y" | bc)
[ $result -eq 1 ] && echo "x大于y" # 输出:x大于y
# 使用awk进行浮点数比较
awk -v x=$x -v y=$y 'BEGIN {print (x > y) ? "x大于y" : "x不大于y"}'
# 定义比较函数
compare_float() {
local x=$1
local y=$2
local result=$(echo "$x > $y" | bc)
[ $result -eq 1 ] && return 1 # x > y
result=$(echo "$x < $y" | bc)
[ $result -eq 1 ] && return 2 # x < y
return 0 # x == y
}
compare_float 3.14 2.71
case $? in
1) echo "第一个数更大";;
2) echo "第二个数更大";;
0) echo "相等";;
esac
4.4 文件属性判断
4.4.1 文件类型判断
| 运算符 | 描述 | 示例 |
|---|---|---|
-e 或 -a |
文件存在 | [ -e file ] |
-f |
普通文件存在 | [ -f file ] |
-d |
目录存在 | [ -d dir ] |
-b |
块设备文件 | [ -b file ] |
-c |
字符设备文件 | [ -c file ] |
-p |
管道文件 | [ -p file ] |
-S |
套接字文件 | [ -S file ] |
-L 或 -h |
符号链接 | [ -L file ] |
bash
# 创建测试文件和目录
touch /tmp/testfile.txt
mkdir -p /tmp/testdir
# 文件存在判断
[ -e /tmp/testfile.txt ] && echo "文件存在"
[ -f /tmp/testfile.txt ] && echo "是普通文件"
[ -d /tmp/testdir ] && echo "是目录"
# 创建符号链接测试
ln -sf /tmp/testfile.txt /tmp/testlink
[ -L /tmp/testlink ] && echo "是符号链接"
[ -e /tmp/testlink ] && echo "链接目标存在"
# 综合判断函数
check_file_type() {
local file=$1
if [ -e "$file" ]; then
if [ -f "$file" ]; then
echo "$file 是普通文件"
elif [ -d "$file" ]; then
echo "$file 是目录"
elif [ -L "$file" ]; then
echo "$file 是符号链接"
elif [ -b "$file" ]; then
echo "$file 是块设备"
elif [ -c "$file" ]; then
echo "$file 是字符设备"
elif [ -p "$file" ]; then
echo "$file 是管道"
elif [ -S "$file" ]; then
echo "$file 是套接字"
else
echo "$file 是其他类型"
fi
else
echo "$file 不存在"
fi
}
4.4.2 文件权限判断
| 运算符 | 描述 | 示例 |
|---|---|---|
-r |
文件可读 | [ -r file ] |
-w |
文件可写 | [ -w file ] |
-x |
文件可执行 | [ -x file ] |
-u |
文件设置了SUID位 | [ -u file ] |
-g |
文件设置了SGID位 | [ -g file ] |
-k |
文件设置了Sticky位 | [ -k file ] |
bash
# 创建测试文件
touch /tmp/testperm
chmod 755 /tmp/testperm
# 权限判断
[ -r /tmp/testperm ] && echo "可读"
[ -w /tmp/testperm ] && echo "可写"
[ -x /tmp/testperm ] && echo "可执行"
# 检查实际权限(考虑实际用户)
# 注意:-w判断的是当前用户的实际权限,而非文件模式
# SUID/SGID/Sticky位测试
chmod 4755 /tmp/testperm # 设置SUID
[ -u /tmp/testperm ] && echo "设置了SUID位"
chmod 2755 /tmp/testperm # 设置SGID
[ -g /tmp/testperm ] && echo "设置了SGID位"
chmod 1755 /tmp/testperm # 设置Sticky
[ -k /tmp/testperm ] && echo "设置了Sticky位"
# 权限判断综合函数
check_permissions() {
local file=$1
local perms=""
[ -r "$file" ] && perms="${perms}r"
[ -w "$file" ] && perms="${perms}w"
[ -x "$file" ] && perms="${perms}x"
echo "权限: $perms"
}
4.4.3 文件属性判断
| 运算符 | 描述 | 示例 |
|---|---|---|
-s |
文件非空(大小大于0) | [ -s file ] |
-N |
文件自上次读取后被修改 | [ -N file ] |
-O |
当前用户拥有文件 | [ -O file ] |
-G |
文件属于当前用户组 | [ -G file ] |
file1 -nt file2 |
file1比file2新 | [ file1 -nt file2 ] |
file1 -ot file2 |
file1比file2旧 | [ file1 -ot file2 ] |
file1 -ef |
file1和file2是同一文件(相同的inode) | [ file1 -ef file2 ] |
bash
# 文件大小判断
touch /tmp/empty.txt
echo "content" > /tmp/notempty.txt
[ -s /tmp/empty.txt ] || echo "empty.txt为空"
[ -s /tmp/notempty.txt ] && echo "notempty.txt非空"
# 文件新旧比较
sleep 1
touch /tmp/newer.txt
[ /tmp/newer.txt -nt /tmp/notempty.txt ] && echo "newer.txt更新"
# 同一文件判断(硬链接或符号链接)
ln /tmp/original.txt /tmp/hardlink.txt
[ /tmp/original.txt -ef /tmp/hardlink.txt ] && echo "是同一文件"
# 文件所有权判断
[ -O /tmp/testfile.txt ] && echo "当前用户拥有此文件"
[ -G /tmp/testfile.txt ] && echo "当前用户组拥有此文件"
# 综合文件检查函数
file_info() {
local file=$1
echo "=== 文件信息: $file ==="
[ -e "$file" ] || { echo "文件不存在"; return 1; }
echo -n "类型: "
[ -f "$file" ] && echo "普通文件" || \
[ -d "$file" ] && echo "目录" || \
[ -L "$file" ] && echo "符号链接" || \
echo "其他"
echo -n "权限: "
echo -n $([ -r "$file" ] && echo "r" || echo "-")
echo -n $([ -w "$file" ] && echo "w" || echo "-")
echo -n $([ -x "$file" ] && echo "x" || echo "-")
echo ""
[ -s "$file" ] && echo "大小: $(stat -c%s "$file" 2>/dev/null || stat -f%z "$file") 字节"
[ -O "$file" ] && echo "所有者: 当前用户"
[ -G "$file" ] && echo "所属组: 当前用户组"
}
4.5 实用判断场景
4.5.1 系统环境判断
bash
#!/bin/bash
# 判断操作系统类型
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "运行在Linux系统上"
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "运行在macOS系统上"
elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]]; then
echo "运行在Windows系统上"
fi
# 判断是否为root用户
if [ $(id -u) -eq 0 ]; then
echo "当前是root用户"
else
echo "当前是普通用户"
fi
# 判断命令是否存在
command_exists() {
command -v "$1" >/dev/null 2>&1
}
if command_exists git; then
echo "git已安装"
fi
if command_exists docker; then
echo "docker已安装"
fi
# 判断Bash版本
if [[ ${BASH_VERSION} < "4.0" ]]; then
echo "Bash版本低于4.0,某些功能可能不可用"
fi
4.5.2 字符串处理判断
bash
#!/bin/bash
# 判断字符串是否包含子串
str="Hello World"
[[ $str == *"World"* ]] && echo "包含World"
# 判断字符串是否以某前缀开头
[[ $str == Hello* ]] && echo "以Hello开头"
# 判断字符串是否以某后缀结尾
filename="document.txt"
[[ $filename == *.txt ]] && echo "是文本文件"
# 判断字符串是否为数字
is_number() {
[[ $1 =~ ^[0-9]+$ ]]
}
is_number "12345" && echo "是数字"
is_number "12a45" || echo "不是数字"
# 判断字符串是否为整数(包括负数)
is_integer() {
[[ $1 =~ ^-?[0-9]+$ ]]
}
is_integer "-123" && echo "是整数"
# 判断字符串是否为浮点数
is_float() {
[[ $1 =~ ^-?[0-9]+\.[0-9]+$ ]]
}
is_float "3.14" && echo "是浮点数"
4.5.3 文件操作判断
bash
#!/bin/bash
# 检查目录是否为空
is_empty_dir() {
[ -d "$1" ] && [ -z "$(ls -A "$1")" ]
}
# 检查文件是否可写
is_writable() {
[ -w "$1" ]
}
# 检查文件是否可执行
is_executable() {
[ -x "$1" ]
}
# 检查两个文件是否为同一文件(通过inode)
is_same_file() {
[ "$1" -ef "$2" ]
}
# 检查文件是否更新
is_newer() {
[ "$1" -nt "$2" ]
}
# 检查文件是否在指定目录
is_in_directory() {
[ "$(dirname "$1")" = "$2" ]
}
4.5.4 网络相关判断
bash
#!/bin/bash
# 检查网络连接
check_network() {
if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1; then
echo "网络连接正常"
return 0
else
echo "网络连接失败"
return 1
fi
}
# 检查端口是否开放
check_port() {
local host=$1
local port=$2
if timeout 2 bash -c "cat < /dev/null > /dev/tcp/$host/$port" 2>/dev/null; then
echo "端口 $port 可访问"
return 0
else
echo "端口 $port 不可访问"
return 1
fi
}
# 检查URL是否可访问
check_url() {
if curl -s --head --fail "$1" >/dev/null 2>&1; then
echo "URL可访问"
return 0
else
echo "URL不可访问"
return 1
fi
}
4.6 综合示例
4.6.1 脚本:检查文件类型和属性
bash
#!/bin/bash
FILE=$1
if [ -z "$FILE" ]; then
echo "用法: $0 <文件名>"
exit 1
fi
echo "检查文件: $FILE"
echo "=============="
# 文件存在性检查
if [ -e "$FILE" ]; then
echo "[✓] 文件存在"
else
echo "[✗] 文件不存在"
exit 1
fi
# 文件类型判断
if [ -f "$FILE" ]; then
echo "[✓] 普通文件"
elif [ -d "$FILE" ]; then
echo "[✓] 目录"
elif [ -L "$FILE" ]; then
echo "[✓] 符号链接"
elif [ -b "$FILE" ]; then
echo "[✓] 块设备"
elif [ -c "$FILE" ]; then
echo "[✓] 字符设备"
else
echo "[?] 未知类型"
fi
# 文件权限检查
echo "--- 权限检查 ---"
[ -r "$FILE" ] && echo "[✓] 可读" || echo "[✗] 不可读"
[ -w "$FILE" ] && echo "[✓] 可写" || echo "[✗] 不可写"
[ -x "$FILE" ] && echo "[✓] 可执行" || echo "[✗] 不可执行"
# 文件大小
if [ -f "$FILE" ]; then
size=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE" 2>/dev/null)
echo "文件大小: $size 字节"
fi
4.6.2 脚本:用户输入验证
bash
#!/bin/bash
# 验证用户名格式(字母开头,后面跟字母数字下划线)
validate_username() {
local username=$1
[[ $username =~ ^[a-zA-Z][a-zA-Z0-9_]*$ ]] && return 0 || return 1
}
# 验证年龄(数字且在合理范围)
validate_age() {
local age=$1
[[ $age =~ ^[0-9]+$ ]] && [ $age -ge 1 ] && [ $age -le 150 ] && return 0 || return 1
}
# 验证邮箱格式
validate_email() {
local email=$1
[[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] && return 0 || return 1
}
# 验证手机号(中国大陆)
validate_phone() {
local phone=$1
[[ $phone =~ ^1[3-9][0-9]{9}$ ]] && return 0 || return 1
}
# 验证IP地址
validate_ip() {
local ip=$1
[[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
local IFS='.'
read -ra parts <<< "$ip"
for part in "${parts[@]}"; do
[ $part -gt 255 ] && return 1
done
return 0
}
# 验证URL
validate_url() {
local url=$1
[[ $url =~ ^https?://[a-zA-Z0-9.-]+(/[a-zA-Z0-9._~:/?#\[\]@!$&'()*+,;=-]*)?$ ]] && return 0 || return 1
}
# 测试
echo "=== 用户输入验证测试 ==="
username="john_doe"
if validate_username "$username"; then
echo "[✓] 用户名 '$username' 合法"
else
echo "[✗] 用户名 '$username' 不合法"
fi
age=25
if validate_age "$age"; then
echo "[✓] 年龄 $age 合法"
else
echo "[✗] 年龄 $age 不合法"
fi
email="user@example.com"
if validate_email "$email"; then
echo "[✓] 邮箱 $email 合法"
else
echo "[✗] 邮箱 $email 不合法"
fi
phone="13812345678"
if validate_phone "$phone"; then
echo "[✓] 手机号 $phone 合法"
else
echo "[✗] 手机号 $phone 不合法"
fi
ip="192.168.1.1"
if validate_ip "$ip"; then
echo "[✓] IP地址 $ip 合法"
else
echo "[✗] IP地址 $ip 不合法"
fi
4.6.3 脚本:数值范围判断
bash
#!/bin/bash
# 判断数值是否在指定范围内
in_range() {
local value=$1
local min=$2
local max=$3
[ $value -ge $min -a $value -le $max ]
}
# 判断成绩等级
get_grade() {
local score=$1
if [ $score -ge 90 ]; then
echo "A"
elif [ $score -ge 80 ]; then
echo "B"
elif [ $score -ge 70 ]; then
echo "C"
elif [ $score -ge 60 ]; then
echo "D"
else
echo "F"
fi
}
# 判断月份对应的季节
get_season() {
local month=$1
case $month in
3|4|5) echo "春季";;
6|7|8) echo "夏季";;
9|10|11) echo "秋季";;
12|1|2) echo "冬季";;
*) echo "无效月份";;
esac
}
# 判断星期几
get_weekday() {
local day=$1
case $day in
1|周一) echo "星期一";;
2|周二) echo "星期二";;
3|周三) echo "星期三";;
4|周四) echo "星期四";;
5|周五) echo "星期五";;
6|周六) echo "星期六";;
0|7|周日) echo "星期日";;
*) echo "无效日期";;
esac
}
# 测试
echo "=== 数值范围判断测试 ==="
value=75
if in_range $value 60 100; then
echo "数值 $value 在有效范围内"
else
echo "数值 $value 超出有效范围"
fi
score=85
grade=$(get_grade $score)
echo "分数 $score 对应等级: $grade"
month=5
season=$(get_season $month)
echo "月份 $month 属于: $season"
weekday=$(get_weekday 3)
echo "星期: $weekday"
4.7 本章小结
本章详细介绍了Shell脚本中的条件判断语法:
-
test命令与条件语法:
test命令和[ ]、[[ ]]的基本用法[ ]与[[ ]]的区别和选择建议(( ))用于数学比较
-
字符串比较:
=、!=:相等和不等判断-z、-n:空值和非空判断- 字典序比较(
<、>) - 变量加引号的重要性
- 模式匹配和正则表达式
-
数值比较:
-eq、-ne、-gt、-ge、-lt、-le- 复合条件判断(
-a、-o或&&、||) - 浮点数比较方法(借助
bc)
-
文件属性判断:
- 文件类型:
-f、-d、-L等 - 文件权限:
-r、-w、-x等 - 文件属性:
-s、-nt、-ot、-ef等 - SUID/SGID/Sticky位
- 文件类型:
-
实用判断场景:
- 系统环境判断
- 字符串处理判断
- 文件操作判断
- 网络相关判断
掌握条件判断是编写实用Shell脚本的基础,熟练运用这些判断语法可以实现各种复杂的逻辑控制。
更多内容,欢迎访问南徽玉的个人博客