目录
一、引言:没有判断的脚本,只能算“批处理”
二、test、[ ]、[[ ]]——三种写法的前世今生
2.1 它们本质上是什么?
2.2 [ ] 的三个强制性规则(新手最容易踩坑)
2.3 [[ ]]:现代Bash的更优选择
三、文件测试:验证“文件是否存在”
四、数字比较与字符串比较
4.1 数字比较
4.2 字符串比较
4.3 [[ ]] 的高级特性
五、if-else 完整语法
5.1 基本结构
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}%" fi4.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的写法是一个单词fi是if的反写,标记结束——这是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,这一点在写可移植脚本时需要特别注意。