模块 :S05 条件表达式
篇号 :S05-03 / 42
预计阅读 :50 分钟
主线 :Bash(必读三星 · 读他人脚本的核心篇)
文章目录
-
- 本篇目标
- [30 秒速览](#30 秒速览)
- 正文
-
- [1. `[[` 是什么](#1.
[[是什么) - [2. 为何脚本里常见 `[[`](#2. 为何脚本里常见
[[) - [3. 基本语法与空格](#3. 基本语法与空格)
- [4. 字符串相等:`=` 与 `==`](#4. 字符串相等:
=与==) - [5. Shell 模式匹配(本篇核心)](#5. Shell 模式匹配(本篇核心))
- [6. 正则匹配:`=~`](#6. 正则匹配:
=~) - [7. 模式 vs 正则:一张表](#7. 模式 vs 正则:一张表)
- [8. 文件测试与空串(与 `[` 对照)](#8. 文件测试与空串(与
[对照)) - [9. 在 `[[` 内部用 `&&` `||` `!`](#9. 在
[[内部用&&||!) - [10. 变量要不要加引号](#10. 变量要不要加引号)
- [11. `[` 与 `[[` 对照速查](#11.
[与[[对照速查) - [12. 读脚本时的典型片段](#12. 读脚本时的典型片段)
-
- [12.1 按环境名分支](#12.1 按环境名分支)
- [12.2 按文件名过滤](#12.2 按文件名过滤)
- [12.3 校验参数格式](#12.3 校验参数格式)
- [12.4 与 `case` 的分工](#12.4 与
case的分工)
- [13. 大小写不敏感(可选)](#13. 大小写不敏感(可选))
- [1. `[[` 是什么](#1.
- 读脚本检查清单
- 练习
- 下一篇预告
本篇目标
掌握 Bash 的 [[ ... ]] :在 if、while 里写更稳、更顺的条件。会用它做字符串相等 、Shell 通配模式匹配 、正则 =~ ,并在 && / || / ! 内组合条件。能对照 [ 与 [[ 的差异,读懂现代 Bash 脚本里的判断写法。
30 秒速览
[[是 Bash 关键字 ,不是外部命令;语法与[不同,不能 随便换成test。- 字符串相等常用
==(也可用=);未加引号的右侧 可当作 Shell 模式 (*?[...])匹配。 =~ regex做正则匹配(右侧建议用变量存正则,避免转义地狱)。- 变量在
[[里 通常不必为防拆词而加引号,但含空格、通配符时仍建议加引号。 &&||!可直接写在[[内部 ;文件测试-f-d等与 S05-02 相同。- 可移植脚本 用
[;Bash 脚本 (#!/usr/bin/env bash)优先[[。
正文
1. [[ 是什么
bash
if [[ -f "$CONFIG" ]]; then
source "$CONFIG"
fi
[[与]]成对出现,中间是条件表达式。- Bash 解析阶段 处理
[[,不是像[那样启动一个名为[的命令。 - 成功(条件为真)→ 退出码 0 ;假 → 1(与 S04-01、S05-01 一致)。
与 [ 的第一条区别:
bash
[ "$a" = "$b" ] # [ 是命令,参数要按「单词」拆开
[[ $a == $b ]] # [[ 由 Bash 解析,规则不同(见下文)
2. 为何脚本里常见 [[
| 能力 | [ / test |
[[ |
|---|---|---|
| 可移植(POSIX sh) | ✅ | ❌ Bash 等 |
右侧 Shell 模式 *.log |
❌(只能比字面串) | ✅ == / != |
正则 =~ |
❌ | ✅ |
内部 **&& ` |
!`** |
|
| 未加引号变量 | 易拆词、易踩坑 | 相对安全(仍建议引号) |
读开源 Bash 脚本时,大量 if [[ ... ]] 是正常现象;若 shebang 是 sh 却满篇 [[,说明作者假设了 Bash(S13)。
3. 基本语法与空格
bash
[[ "$name" == "prod" ]]
[[ -f "$file" && -r "$file" ]]
[[ ! -d "$DIR" ]]
[[后、]]前 要有条件;运算符两侧通常加空格(与[类似,便于阅读)。- 不要 写成
[[-f file]](会解析失败)。
4. 字符串相等:= 与 ==
在 [[ 里 ,= 与 == 都表示字符串相等(Bash 中二者等价):
bash
env="staging"
[[ "$env" = "prod" ]] && echo "prod"
[[ "$env" == "staging" ]] && echo "staging" # 输出 staging
POSIX 的 [ 里请用 = 做字符串比较(S05-01);== 在 [ 里是历史扩展,别依赖。
整数大小 不要写在 [[ 里用 > <(那是重定向符号)。用 -eq -lt 或 (( ))(S05-04):
bash
n=10
[[ "$n" -lt 20 ]] && echo "ok"
(( n >= 10 )) && echo "ok"
5. Shell 模式匹配(本篇核心)
当 == 或 != 右侧未加引号 时,Bash 把右侧当作 Shell 模式(pathname expansion 同款规则,不是正则):
| 模式元字符 | 含义 |
|---|---|
* |
任意长度任意字符 |
? |
恰好一个字符 |
[abc] |
匹配括号内任一字符 |
[!abc] 或 [^abc] |
不匹配括号内字符 |
bash
file="app.log"
[[ $file == *.log ]] && echo "日志文件" # 真
host="api-v2.example.com"
[[ $host == api-* ]] && echo "api 前缀" # 真
[[ $host != prod-* ]] && echo "非 prod 主机名"
右侧加引号 → 只做字面字符串 比较,不做模式匹配:
bash
pattern='*.log'
[[ $file == "$pattern" ]] # 假:比的是字面量 *.log,不是后缀 .log
[[ $file == $pattern ]] # 真:右侧是模式 *.log
左侧 一般也会参与匹配语义;变量含 *、? 时要小心:
bash
# 若 user 未加引号且值为 a*b,* 会按模式解释
[[ $user == admin-* ]]
稳妥写法 :左侧加引号,需要模式时只让右侧承担通配:
bash
[[ "$file" == *.log ]]
[[ "$name" == [Pp]rod ]] # 匹配 prod 或 Prod
6. 正则匹配:=~
bash
email="user@example.com"
[[ $email =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]] && echo "像邮箱"
line="error: timeout"
[[ $line =~ ^error: ]] && echo "错误行"
要点:
| 项 | 说明 |
|---|---|
| 运算符 | =~ (不是 ==) |
| 右侧 | 扩展正则(ERE),不是 Shell 模式 |
| 引用 | 右侧加双引号 时,部分版本会按字面串 比,不要随便给整个正则加引号 |
| 推荐 | 正则放进变量,右侧写 $re 或 "$re"(按你需要的字面/正则语义选) |
bash
re='^[0-9]+$'
val="42"
[[ $val =~ $re ]] && echo "纯数字"
# 从用户输入读模式时,用变量,避免在 [[ 行里手写大量反斜杠
捕获分组 (Bash 3.2+):匹配成功后可用 BASH_REMATCH:
bash
ver="v1.2.3"
if [[ $ver =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "major=${BASH_REMATCH[1]}" # 1
echo "full=${BASH_REMATCH[0]}" # 整段匹配
fi
7. 模式 vs 正则:一张表
| 需求 | 写法 | 右侧类型 |
|---|---|---|
后缀是 .log |
[[ $f == *.log ]] |
Shell 模式 |
主机名像 api- 开头 |
[[ $h == api-* ]] |
Shell 模式 |
| 纯数字 | [[ $n =~ ^[0-9]+$ ]] |
正则 |
| 邮箱粗略校验 | [[ $e =~ @.+ ]] |
正则 |
Shell 模式 简单、快;正则 表达力强。不要写 [[ $x == ^[0-9]+$ ]](那是把 ^ 当普通字符去模式匹配,不是正则)。
8. 文件测试与空串(与 [ 对照)
文件谓词 与 S05-02 相同,可直接写在 [[ 里:
bash
[[ -f "$CONFIG" && -r "$CONFIG" ]]
[[ -d "$OUT" || -L "$OUT" ]]
空串:
bash
[[ -z "$OPT" ]]
[[ -n "${1:-}" ]]
组合示例(读脚本高频):
bash
if [[ -f "$LOCK" && -s "$LOCK" ]]; then
echo "已有非空锁文件"
fi
if [[ ! -d "$WORKDIR" ]]; then
mkdir -p "$WORKDIR"
fi
9. 在 [[ 内部用 && || !
bash
[[ "$env" == "prod" && -n "$API_KEY" ]]
[[ -z "$f" || -z "$g" ]]
[[ ! "$SKIP" == "yes" ]]
对比 POSIX 用 -a -o 或两个 [:
bash
# 老式
[ "$env" = "prod" -a -n "$API_KEY" ]
# 更清晰(仍用 [)
[ "$env" = "prod" ] && [ -n "$API_KEY" ]
# Bash 脚本里常合并为一个 [[
[[ "$env" == "prod" && -n "$API_KEY" ]]
! 取反整个子表达式或谓词:
bash
[[ ! -f "$SKIP_FILE" ]] && run_job
10. 变量要不要加引号
| 场景 | 建议 |
|---|---|
| 普通字符串比较 | "$var" |
| 右侧要当 模式 | 右侧不加引号 ;左侧建议 "$var" |
| 正则 | 用 $re 变量 ;慎给含 \ 的整段加引号 |
| 文件路径 | 始终 "$path" |
bash
# 未定义变量
[[ -n "${OPT:-}" ]]
# set -u 下安全
[[ "${DEBUG:-}" == "1" ]]
[[ 不会对未加引号的变量做 pathname 展开 (不会像裸写 $f 在命令行里那样扫目录),但仍可能触发模式匹配(上一节)。
11. [ 与 [[ 对照速查
| 写法 | [ |
[[ |
|---|---|---|
| 字符串相等 | [ "$a" = "$b" ] |
[[ "$a" == "$b" ]] |
| 模式匹配 | 不支持(除非外部 case) |
[[ "$a" == foo* ]] |
| 正则 | 不支持 | [[ "$a" =~ ^foo ]] |
| 与 / 或 | -a -o 或 && 两个 [ |
[[ a && b ]] |
| 存在文件 | [ -f "$f" ] |
[[ -f "$f" ]] |
| 命令形式 | test / [ 命令 |
关键字,无 test 等价 |
不要混用语法:
bash
# 错:[[ 里抄 POSIX 的 = 有时可以,但把 [ 的 -a 带进 [[ 会乱
[[ "$a" = "$b" -a -n "$c" ]] # 避免 -a,改用 &&
# 错:把 [[ 当成命令去跑
/usr/bin/[[ -f file ]] # 不存在这种用法
12. 读脚本时的典型片段
12.1 按环境名分支
bash
if [[ "$DEPLOY_ENV" == "prod" || "$DEPLOY_ENV" == "production" ]]; then
STRICT=1
fi
12.2 按文件名过滤
bash
for f in *.tar.gz; do
[[ -f "$f" && "$f" == release-*.tar.gz ]] || continue
process "$f"
done
12.3 校验参数格式
bash
if [[ ! "$PORT" =~ ^[0-9]+$ ]]; then
echo "PORT must be numeric" >&2
exit 1
fi
12.4 与 case 的分工
| 场景 | 更合适的工具 |
|---|---|
| 多个离散取值 | case(S06-02) |
| 前缀/后缀通配 | [[ == pattern ]] |
| 复杂结构校验 | [[ =~ regex ]] |
13. 大小写不敏感(可选)
默认 区分大小写 。打开 shopt -s nocasematch 后,[[ 的模式匹配 与 case 不区分大小写:
bash
shopt -s nocasematch
[[ "$ans" == y ]] && echo "yes" # Y、y 都行
shopt -u nocasematch
=~ 是否受 nocasematch 影响 因 Bash 版本而异;敏感逻辑请用显式字符类 [Yy]。
读脚本检查清单
- shebang 是
bash还是sh?[[仅适用于前者生态。 - 想匹配
*.log时,右侧是否误加了引号变成字面量? - 需要正则 时是否用了
=~,而不是== ^...$? - 比数值是否误用了
>(应-lt或(( )))? - 路径、含空格变量是否仍加了
"$var"? - 复杂条件是一个
[[ ... && ... ]]还是多个[嵌套?能否读懂短路?
练习
判断题
[[和[一样,都是/usr/bin/[这条命令。[[ $f == "*.log" ]]能判断$f是否以.log结尾。[[ $n =~ ^[0-9]+$ ]]表示用正则判断$n是否为纯数字。[[ -f "$a" && -r "$a" ]]表示文件存在、可读且为普通文件。- 在
[里应优先用==做字符串比较以与[[一致。
参考答案
- 错(
[[是 Bash 关键字)。 - 错(右侧有引号,比的是字面
*.log)。 - 对。
- 对。
- 错(
[里用=;==在[中不可靠)。
实操题 1:模式匹配
bash
names=(app.log app.txt access.log README)
for n in "${names[@]}"; do
if [[ $n == *.log ]]; then
echo "log: $n"
fi
done
在本地运行,写出输出;再给 n=release-1.0.log 单独测 [[ $n == release-*.log ]]。
参考答案
输出:
text
log: app.log
log: access.log
release-1.0.log 与 release-*.log 匹配为真。
实操题 2:正则与 BASH_REMATCH
bash
tag="build-20240519-rc1"
if [[ $tag =~ ^build-([0-9]{8})-(rc[0-9]+)$ ]]; then
echo "date=${BASH_REMATCH[1]} rel=${BASH_REMATCH[2]}"
fi
写出 echo 行内容。
参考答案
date=20240519 rel=rc1
改错题
bash
#!/bin/sh
file="$1"
if [[ $file == "*.log" ]]; then
gzip "$file"
fi
if [[ $count > 10 ]]; then
echo "many"
fi
if [[ $id =~ "^[0-9]+$" ]]; then
echo "numeric id"
fi
参考
bash
#!/usr/bin/env bash
file="$1"
if [[ "$file" == *.log ]]; then
gzip -- "$file"
fi
if (( count > 10 )); then
echo "many"
fi
re='^[0-9]+$'
if [[ $id =~ $re ]]; then
echo "numeric id"
fi
要点:shebang 与 [[ 一致;模式右侧无引号;数值用 (( ));正则用变量避免给整段加引号。
读脚本题
说明下面两段各自为真时 $name 的大致形态:
bash
# A
[[ $name == api-* ]]
# B
[[ $name =~ ^api-[a-z0-9]+$ ]]
参考答案
- A :Shell 模式,
api-开头,后面任意字符(如api-v1、api-)。 - B :正则,整体须为
api-+ 一串小写字母或数字(如api-v2,api-V2不匹配)。
下一篇预告
S05-04 :《算术判断 (( )) 与 let:数值比较与自增》--- 在条件里写 (( n > 0 ))、((i++)),与 [[、-eq 的分工。