【Linux从入门到精通】第23篇:条件判断——让脚本拥有“大脑”

目录

一、引言:没有判断的脚本,只能算"批处理"

[二、test、[ ]、[[ ]]------三种写法的前世今生](#二、test、[ ]、[[ ]]——三种写法的前世今生)

[2.1 它们本质上是什么?](#2.1 它们本质上是什么?)

[2.2 [ ] 的三个强制性规则(新手最容易踩坑)](#2.2 [ ] 的三个强制性规则(新手最容易踩坑))

[2.3 [[ ]]:现代Bash的更优选择](#2.3 [[ ]]:现代Bash的更优选择)

三、文件测试:验证"文件是否存在"

四、数字比较与字符串比较

[4.1 数字比较](#4.1 数字比较)

[4.2 字符串比较](#4.2 字符串比较)

[4.3 [[ ]] 的高级特性](#4.3 [[ ]] 的高级特性)

[五、if-else 完整语法](#五、if-else 完整语法)

[5.1 基本结构](#5.1 基本结构)

[5.2 一行写法](#5.2 一行写法)

六、综合实战:系统巡检脚本

七、本篇小结

动手练习

八、下篇预告


一、引言:没有判断的脚本,只能算"批处理"

回顾一下前面写的脚本:

bash

复制代码
#!/bin/bash
name="zhangsan"
echo "Hello, ${name}"

这其实只是一系列命令的顺序执行------和逐条在终端输入没本质区别。真正让脚本有价值的,是判断

  • 如果 备份目录不存在, 创建它;否则直接开始备份

  • 如果 磁盘使用率超过90%, 发送告警;否则记录正常日志

  • 如果 用户输入了参数, 使用参数值;否则使用默认值

Shell本身没有"花括号代码块"的复杂语法,它的条件判断通过条件测试命令来实现。这是Shell编程中最容易让人困惑的地方之一------因为它有好几种写法,长得还挺像。

二、test、[ ]、[[ ]]------三种写法的前世今生

2.1 它们本质上是什么?

三者都是用来判断条件是否为真,但它们的历史和特性不同:

写法 本质 出现年代 特性
test 条件 独立命令 Unix V7(1979) 最原始,功能最基础
[ 条件 ] test命令的别名 Unix V7 本质上和test完全一样 ,但它必须 有一个配对的]
[[ 条件 ]] Bash内置关键字 Bash 2.02(1998) 功能更强 ,支持正则、&&、`

关键认知[不是语法符号,而是一个实实在在的命令!执行which [,你会发现它在/usr/bin/[。当你写[ -f /etc/passwd ]时,Shell实际上在执行/usr/bin/[这个程序,]只是它的最后一个参数。

2.2 [ ] 的三个强制性规则(新手最容易踩坑)

因为[是一个命令,它在语义上和test完全没有区别。但它有严格的语法要求:

规则一:[] 两边必须有空格

bash

复制代码
[ -f /etc/passwd ]     # 正确
[-f /etc/passwd]       # 错误!Shell把[-f当成一个整体去执行
[ -f /etc/passwd]      # 错误!缺少]前的空格

规则二:[ ]内使用&&||不可靠,应该用-a-o

bash

复制代码
# 在 [ ] 中,用 -a(and)和 -o(or)
[ "$age" -gt 18 -a "$age" -lt 60 ]

# 如果用 && 和 ||,必须写在 [ ] 外面
[ "$age" -gt 18 ] && [ "$age" -lt 60 ]

规则三:变量引用必须加双引号,防止空值导致语法错误

bash

复制代码
name=""   # 假设变量为空

[ $name = "zhangsan" ]        # 展开后变成: [ = "zhangsan" ] → 语法错误!
[ "$name" = "zhangsan" ]      # 展开后变成: [ "" = "zhangsan" ] → 正确

为什么Bash新手总遇到 "unary operator expected" 错误?

根本原因就是第三条------变量为空时没加双引号:

bash

复制代码
$ var=""
$ [ $var = "hello" ]
bash: [: =: unary operator expected
# 展开后变成了 [ = "hello" ],[ 命令看到三个参数:=、hello、]
# 第一个参数是 = 而不是操作符的一元表达式,于是报错

2.3 [[ ]]:现代Bash的更优选择

[[ ]]是Bash内置关键字,不是外部命令,因此它:

bash

复制代码
# 1. 变量不需要加引号也不怕空值
name=""
[[ $name = "zhangsan" ]] && echo "匹配"  # 不会报错,安全

# 2. 支持 && 和 || 直接写在里面
[[ $age -gt 18 && $age -lt 60 ]]

# 3. 支持正则表达式
[[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]

# 4. 支持模式匹配(像通配符一样的匹配)
[[ $filename == *.log ]]   # 匹配所有.log文件

选择建议

场景 推荐 理由
新写Bash脚本 [[ ]] 更安全、更强大、不会因空值报错
需要兼容sh/dash [ ] BusyBox等精简环境可能只支持[ ]
一行简单测试 [ ] 最常见,大部分教程和脚本都在用

本文之后的内容会混用两种写法,以帮助你在阅读旧脚本时能看懂。但你自己写脚本时,优先使用[[ ]]

三、文件测试:验证"文件是否存在"

文件测试是运维脚本中最高频的判断------你要备份,得先确认目录存在;你要清理日志,得先确认日志文件还在。

操作符 含义 示例
-f 是否为普通文件(不是目录、不是设备) [[ -f /etc/passwd ]]
-d 是否为目录 [[ -d /var/log ]]
-e 存在即可(不管是文件还是目录) [[ -e /tmp/maybe_empty ]]
-r 是否可读 [[ -r /etc/shadow ]]
-w 是否可写 [[ -w /var/log/app.log ]]
-x 是否可执行 [[ -x /usr/bin/python3 ]]
-s 文件存在且大小大于0 [[ -s /var/log/app.log ]]
-L 是否为符号链接 [[ -L /usr/bin/python ]]
-nt 文件A 文件B(newer than) [[ file1 -nt file2 ]]
-ot 文件A 文件B(older than) [[ file1 -ot file2 ]]

实战示例

bash

复制代码
#!/bin/bash
BACKUP_DIR="/backup/database"

# 如果备份目录不存在,就创建它
if [[ ! -d "$BACKUP_DIR" ]]; then
    echo "创建备份目录: ${BACKUP_DIR}"
    mkdir -p "$BACKUP_DIR"
fi

# 如果配置文件不可读,报错退出
if [[ ! -r "/etc/myapp/config.yaml" ]]; then
    echo "错误:无法读取配置文件!"
    exit 1
fi

# 如果日志文件为空,跳过分析
if [[ ! -s "/var/log/app.log" ]]; then
    echo "日志文件为空,跳过分析"
    exit 0
fi

-e-f/-d的区分

bash

复制代码
# -e 只判断存在性,不关心类型
[[ -e /tmp/something ]]   # 不管它是文件、目录、设备,存在即真

# -f 和 -d 则明确指定类型
[[ -f /etc/passwd ]]      # 是普通文件才为真,目录为假
[[ -d /etc ]]             # 是目录才为真,文件为假

四、数字比较与字符串比较

4.1 数字比较

数字比较使用特定的操作符(不是数学符号):

操作符 含义 示例
-eq 等于(equal) [[ $a -eq $b ]]
-ne 不等于(not equal) [[ $a -ne $b ]]
-gt 大于(greater than) [[ $a -gt $b ]]
-lt 小于(less than) [[ $a -lt $b ]]
-ge 大于等于(greater or equal) [[ $a -ge $b ]]
-le 小于等于(less or equal) [[ $a -le $b ]]

bash

复制代码
#!/bin/bash
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')

if [[ $disk_usage -gt 90 ]]; then
    echo "警告:磁盘使用率超过90%!当前:${disk_usage}%"
elif [[ $disk_usage -gt 80 ]]; then
    echo "注意:磁盘使用率超过80%,当前:${disk_usage}%"
else
    echo "磁盘使用率正常:${disk_usage}%"
fi

4.2 字符串比较

操作符 含义 [ ] [[ ]]
=== 等于 都支持 都支持
!= 不等于 都支持 都支持
-z 字符串长度为0(为空) 都支持 都支持
-n 字符串长度不为0(非空) 都支持 都支持
> < 字典序比较 需要用\>转义 直接使用
=~ 正则匹配 不支持 支持

bash

复制代码
#!/bin/bash
read -p "请输入你的名字: " name

# 判断是否为空
if [[ -z "$name" ]]; then
    echo "名字不能为空!"
    exit 1
fi

# 判断是否等于管理员
if [[ "$name" = "admin" ]]; then
    echo "欢迎管理员!"
else
    echo "欢迎普通用户:${name}"
fi

-z-n的记忆技巧-z是"zero length",-n是"non-zero"。

4.3 [[ ]] 的高级特性

正则匹配 (这是[[ ]]独有的):

bash

复制代码
#!/bin/bash
read -p "请输入邮箱地址: " email

# 简单的邮箱格式校验
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "邮箱格式正确"
else
    echo "邮箱格式错误!"
fi

通配符模式匹配

bash

复制代码
filename="backup_2026.tar.gz"

# 匹配特定模式(不需要用grep)
if [[ "$filename" == backup_* ]]; then
    echo "这是一个备份文件"
fi

if [[ "$filename" == *.tar.gz ]]; then
    echo "这是一个压缩包"
fi

五、if-else 完整语法

5.1 基本结构

bash

复制代码
#!/bin/bash

# 单分支
if [[ 条件 ]]; then
    命令
fi

# 双分支
if [[ 条件 ]]; then
    命令
else
    命令
fi

# 多分支
if [[ 条件1 ]]; then
    命令1
elif [[ 条件2 ]]; then
    命令2
else
    命令3
fi

关键语法点

  • then可以和if写在同一行(前面的分号是换行符的替代)

  • elif不是else if,Shell的写法是一个单词

  • fiif的反写,标记结束------这是Shell特有的风格

5.2 一行写法

bash

复制代码
# 命令成功则执行(&&)
[[ -d /backup ]] && echo "备份目录存在"

# 命令失败则执行(||)
grep "error" app.log || echo "没有找到错误日志"

# 组合
[[ $debug -eq 1 ]] && echo "调试模式开启" || echo "正常模式"

六、综合实战:系统巡检脚本

把今天学的条件判断和第22篇的字符串处理结合起来:

bash

复制代码
#!/bin/bash
# 系统巡检脚本 - 检查关键指标并在异常时告警

# 配置(可修改)
THRESHOLD_CPU=80
THRESHOLD_MEM=80
THRESHOLD_DISK=90
LOG_FILE="/var/log/system_check.log"

# 初始化日志(不存在则创建)
[[ ! -d "$(dirname "$LOG_FILE")" ]] && mkdir -p "$(dirname "$LOG_FILE")"

echo "=== 系统巡检 $(date '+%Y-%m-%d %H:%M:%S') ===" | tee -a "$LOG_FILE"

# 1. 检查磁盘空间
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [[ $disk_usage -ge $THRESHOLD_DISK ]]; then
    echo "[严重] 磁盘使用率: ${disk_usage}%(阈值: ${THRESHOLD_DISK}%)" | tee -a "$LOG_FILE"
elif [[ $disk_usage -ge $((THRESHOLD_DISK - 10)) ]]; then
    echo "[警告] 磁盘使用率: ${disk_usage}%(阈值: ${THRESHOLD_DISK}%)" | tee -a "$LOG_FILE"
else
    echo "[正常] 磁盘使用率: ${disk_usage}%" | tee -a "$LOG_FILE"
fi

# 2. 检查内存
mem_usage=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}')
if [[ $mem_usage -ge $THRESHOLD_MEM ]]; then
    echo "[严重] 内存使用率: ${mem_usage}%" | tee -a "$LOG_FILE"
else
    echo "[正常] 内存使用率: ${mem_usage}%" | tee -a "$LOG_FILE"
fi

# 3. 检查服务状态
services=("nginx" "mysql" "sshd")
for svc in "${services[@]}"; do
    if systemctl is-active --quiet "$svc"; then
        echo "[正常] 服务 ${svc} 正在运行" | tee -a "$LOG_FILE"
    else
        echo "[严重] 服务 ${svc} 已停止!" | tee -a "$LOG_FILE"
    fi
done

# 4. 判断是否存在僵尸进程(超过5个需要关注)
zombie_count=$(ps aux | awk '$8 ~ /Z/ {print}' | wc -l)
if [[ $zombie_count -gt 5 ]]; then
    echo "[警告] 僵尸进程数量: ${zombie_count}" | tee -a "$LOG_FILE"
elif [[ $zombie_count -gt 0 ]]; then
    echo "[提示] 僵尸进程数量: ${zombie_count}(少量无需处理)" | tee -a "$LOG_FILE"
fi

echo "巡检完成" | tee -a "$LOG_FILE"

这个脚本整合了:变量默认值、文件存在性检查、数字比较、命令替换、服务状态判断------涵盖了本篇大部分核心内容。

七、本篇小结

三种写法比较

特性 test [ ] [[ ]]
本质 命令 test的别名 Bash关键字
空值安全 需要引号 需要引号 自动安全
正则匹配 不支持 不支持 支持=~
&&/` ` 在外部使用
推荐场合 不再推荐 兼容老脚本 新脚本默认选择

文件测试-f文件、-d目录、-e存在、-r可读、-w可写、-x可执行、-s非空

数字比较-eq等、-ne不等、-gt大于、-lt小于、-ge/-le

字符串比较=等、!=不等、-z为空、-n非空

动手练习

bash

复制代码
#!/bin/bash
# 练习:写一个脚本判断用户输入的参数

# 1. 判断是否有参数传入
if [[ $# -eq 0 ]]; then
    echo "用法:$0 <文件路径>"
    exit 1
fi

# 2. 判断路径是否存在
if [[ -e "$1" ]]; then
    echo "✓ $1 存在"

    # 3. 判断是文件还是目录
    if [[ -f "$1" ]]; then
        echo "  类型:文件"
        echo "  大小:$(du -h "$1" | cut -f1)"
    elif [[ -d "$1" ]]; then
        echo "  类型:目录"
        echo "  内容数量:$(ls -1 "$1" | wc -l) 项"
    fi

    # 4. 判断权限
    [[ -r "$1" ]] && echo "  ✓ 可读" || echo "  ✗ 不可读"
    [[ -w "$1" ]] && echo "  ✓ 可写" || echo "  ✗ 不可写"
    [[ -x "$1" ]] && echo "  ✓ 可执行" || echo "  ✗ 不可执行"
else
    echo "✗ $1 不存在"
fi

八、下篇预告

掌握了条件判断,脚本已经能应对单次决策。但很多任务需要根据多条件分支选择不同的执行路径 ,比如"根据用户输入执行不同的操作"------这正是case语句的用武之地。

下一篇我们将正式学习if-else全面语法与case多分支选择,通过编写一个服务管理脚本(支持start/stop/restart/status),将条件判断提升到实战级别。


延伸思考 :有些人在[ ]中用==,有些人用=。在Bash中两者完全等价,但在纯sh(如dash)中只有=是标准写法。为了最大兼容性,习惯用=更好。而[[ ]]是Bash独有的,不兼容标准sh,这一点在写可移植脚本时需要特别注意。

相关推荐
张青贤2 小时前
Docker Remote 未授权访问漏洞修复方法
运维·docker·容器
feng_you_ying_li2 小时前
linu之进程的程序替换与shell基本实现的基本版本
linux
生物信息与育种2 小时前
JIPB | 一个表观多组学整合分析与可视化工具OmicsCanvas
运维·人工智能·算法·自动化·transformer
汤愈韬2 小时前
防火墙双击热备的工作模式详解
运维·服务器
humors2212 小时前
SSH管理github代码
运维·ssh·github
念恒123062 小时前
进程控制---进程程序替换
linux·c语言
不羁的fang少年2 小时前
https机制
服务器·https·ssl
AKA__Zas2 小时前
初识多线程(初初识)
java·服务器·开发语言·学习方法
小夏子_riotous2 小时前
Docker学习路径——10、Docker Compose 一站式编排:从入门到生产级部署
linux·运维·服务器·docker·容器·centos·云计算