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