Shell条件判断与运算符:test、[ ]、、if/case实战
脚本里光按顺序执行命令远远不够,得能根据情况分叉:文件存在就备份,磁盘超 80% 就告警,参数是-v就打印详细日志。这些都靠条件判断。这一篇把if、test、[ ]、[[ ]]和case一次讲透,重点是几组运算符的真实区别和最容易写错的空格。
一、if 语句的基本结构
if [ 条件 ]; then 命令 elif [ 条件 ]; then 命令 else 命令 fi注意三个语法点:if后面那一行必须以then结尾(同一行写就用分号隔开);整段以fi收尾;条件部分和方括号之间必须有空格。
#!/bin/bash score=85 if [ $score -ge 90 ]; then echo "优秀" elif [ $score -ge 60 ]; then echo "及格" else echo "挂科" fi预期输出:
及格二、test 命令与 [ ]:它们其实是同一个东西
[是一个真实存在的命令(在/usr/bin/[,bash 也有内置版本),它的任务是"判断条件,结果成立返回 0,不成立返回 1"。test命令和它等价,只是写法不同。
test -f /etc/hostname && echo "文件存在" [ -f /etc/hostname ] && echo "文件存在"这两行效果完全一样。方括号末尾的]只是[命令要求的最后一个参数,纯粹是为了语法对称。
⚠️ 空格是硬要求
[ -f /etc/hostname ] # 正确 [-f /etc/hostname] # 错误:shell 把 [-f 当成一个命令名 [ -f /etc/hostname] # 错误:] 前面没空格 [ -f "/etc/hostname" ] # 正确写成[-f /etc/hostname],shell 会去找一个叫[-f的可执行文件,报command not found。这是新手写if第一周最高频的报错。
三、文件测试运算符
判断文件状态是脚本里用得最多的一类条件。
| 运算符 | 判断为真的条件 |
|---|---|
-e 路径 | 文件或目录存在 |
-f 路径 | 存在且是普通文件 |
-d 路径 | 存在且是目录 |
-r 路径 | 当前用户可读 |
-w 路径 | 可写 |
-x 路径 | 可执行 |
-s 路径 | 存在且大小非 0 |
-L 路径 | 是符号链接 |
完整示例:写一个脚本检查配置文件,不存在就自动建一个。
#!/bin/bash config="$HOME/myapp.conf" if [ ! -f "$config" ]; then echo "配置文件不存在,创建默认配置" echo "debug=false" > "$config" else echo "配置文件已存在,跳过" fi ls -l "$config"第一次运行预期输出:
配置文件不存在,创建默认配置 -rw-r--r-- 1 user user 12 Sep 16 10:00 /home/user/myapp.conf再跑一次:
配置文件已存在,跳过 -rw-r--r-- 1 user user 12 Sep 16 10:00 /home/user/myapp.conf!是逻辑非,放在条件前面,注意它和[之间也要有空格。
四、字符串与数值比较
字符串比较
| 写法 | 含义 |
|---|---|
[ "$a" = "$b" ] | 两个字符串相等 |
[ "$a" != "$b" ] | 不相等 |
[ -z "$s" ] | 字符串为空(长度为 0) |
[ -n "$s" ] | 字符串非空 |
#!/bin/bash user="" if [ -z "$user" ]; then echo "user 是空的,用默认值 guest" user="guest" fi echo "当前用户: $user"预期输出:
user 是空的,用默认值 guest 当前用户: guest数值比较
数值比较不能用><,因为那两个符号在 shell 里是重定向。数值比较用专用字母:
| 写法 | 含义 | 对应数学符号 |
|---|---|---|
-eq | 等于 | == |
-ne | 不等 | != |
-lt | 小于 | < |
-le | 小于等于 | ≤ |
-gt | 大于 | > |
-ge | 大于等于 | ≥ |
#!/bin/bash used=$(df / | awk 'NR==2{print $5}' | tr -d '%') echo "根分区已用 $used%" if [ "$used" -ge 85 ]; then echo "告警:磁盘使用率超过 85%" fi磁盘用了 72% 时预期输出:
根分区已用 72%如果是 90%:
根分区已用 90% 告警:磁盘使用率超过 85%⚠️ 变量为空时的坑
[ $num -ge 85 ]这种写法在num为空时会报错:shell 把它展开成[ -ge 85 ],[收到的参数不够。防御性写法是给变量补默认值:
used=${used:-0} if [ "$used" -ge 85 ]; then${used:-0}的意思是:used没定义或为空就用 0,这是写生产脚本的标准习惯。
五、:增强版条件判断
[[ ]]是 bash 对[ ]的扩展,比 POSIX 标准的[ ]更安全、功能更多。
| 对比项 | [ ] | [[ ]] |
|---|---|---|
| 变量未加引号 | 容易因空格分词出错 | 安全,不分词 |
| 逻辑与 | 用-a,易出错 | 直接用&& |
| 逻辑或 | 用-o | 直接用\|\| |
| 正则匹配 | 不支持 | [[ $str =~ ^[0-9]+$ ]] |
| 通配符匹配 | 不支持 | [[ $file == *.log ]] |
| 可移植性 | 所有 shell 都支持 | 仅 bash/zsh/ksh |
#!/bin/bash filename="access.log" if [[ $filename == *.log ]]; then echo "$filename 是日志文件" fi ip="192.168.1.100" if [[ $ip =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then echo "$ip 格式像一个 IPv4 地址" fi预期输出:
access.log 是日志文件 192.168.1.100 格式像一个 IPv4 地址什么时候用哪个?写要兼容sh的脚本(比如 Docker 里的/bin/sh)只能用[ ];自己机器上跑的 bash 脚本,能用[[ ]]就用它,少写很多引号。
六、逻辑组合:!、&&、||
条件之间要"与或非",有两种写法。
第一种,在[ ]内部用-a(and)和-o(or):
if [ -f "$file" -a -x "$file" ]; then echo "$file 存在且可执行" fi第二种,在两个[ ]外面用&&和||(更推荐):
if [ -f "$file" ] && [ -x "$file" ]; then echo "$file 存在且可执行" fi第二种在[[ ]]里还能写进同一对方括号:
if [[ -f $file && -x $file ]]; then echo "$file 存在且可执行" fi&&和||还有一种"短路"用法,把它写在命令行里省掉整个 if:
[ -d /tmp/cache ] && echo "缓存目录存在" mkdir -p /tmp/cache || echo "建目录失败"第一条:[ ]成立才执行后面的 echo。第二条:mkdir失败才执行 echo。这种写法适合一行搞定的简单分支,复杂逻辑还是老实写if。
七、case 语句:多分支的更优解
分支超过三个,写一串if elif elif会很难读。case就是为这种场景准备的。
case 变量 in 模式1) 命令 ;; 模式2) 命令 ;; *) 其他情况 ;; esac每个分支以;;结束(相当于其他语言的break),*是兜底分支。模式支持通配符:
#!/bin/bash case "$1" in start) echo "启动服务..." ;; stop) echo "停止服务..." ;; restart) echo "重启服务..." ;; status) echo "查看状态..." ;; "") echo "用法: $0 {start|stop|restart|status}" exit 1 ;; *) echo "未知动作: $1" echo "用法: $0 {start|stop|restart|status}" exit 1 ;; esac执行几次看效果:
./service.sh start ./service.sh status ./service.sh ./service.sh reload预期输出依次为:
启动服务... 查看状态... 用法: ./service.sh {start|stop|restart|status} 未知动作: reload 用法: ./service.sh {start|stop|restart|status}case的模式还支持|分隔多个值:
case "$ext" in jpg|jpeg|png|gif) echo "图片文件" ;; txt|md) echo "文本文件" ;; esac八、知识扩展:[到底是命令还是语法
很多人以为[是 shell 的语法关键字,其实不是。它是一个外部命令,只是 bash 又内置了一份同名实现用来提速。
用type命令能看出来:
type [ type test预期输出:
[ is a shell builtin test is a shell builtin[本质上就是test,唯一区别是[要求最后一个参数必须是]。你甚至可以调用/usr/bin/[这个二进制文件来验证:
/usr/bin/[ -f /etc/hostname ] && echo ok这解释了为什么[ ]对空格这么敏感——它是普通命令,参数必须用空格分开,和ls -l /tmp没区别。而[[ ]]才是真正的 shell 语法,解析由 bash 自己完成,所以它不用在意分词,还支持&&、=~这些高级特性。
另一个值得知道的细节:if本身不判断真假,它只是执行if后面那整条命令,看它的退出码。[ ... ]返回 0 表示条件成立,if就走 then 分支。理解这一点,你就能写出更灵活的判断:
if grep -q "error" /var/log/syslog; then echo "日志里有 error" fi这里根本没用[ ],而是直接用grep -q的退出码做判断——找到了返回 0,走 then 分支。这种写法比先grep | wc -l再比较数字干净得多。