Shell脚本精读 · S05-03 | `[[` 与模式匹配:Bash 条件表达式

模块 :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:模式匹配)
      • [实操题 2:正则与 BASH_REMATCH](#实操题 2:正则与 BASH_REMATCH)
      • 改错题
      • 读脚本题
    • 下一篇预告

本篇目标

掌握 Bash 的 [[ ... ]] :在 ifwhile 里写更稳、更顺的条件。会用它做字符串相等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"
  • 复杂条件是一个 [[ ... && ... ]] 还是多个 [ 嵌套?能否读懂短路?

练习

判断题

  1. [[[ 一样,都是 /usr/bin/[ 这条命令。
  2. [[ $f == "*.log" ]] 能判断 $f 是否以 .log 结尾。
  3. [[ $n =~ ^[0-9]+$ ]] 表示用正则判断 $n 是否为纯数字。
  4. [[ -f "$a" && -r "$a" ]] 表示文件存在、可读且为普通文件。
  5. [ 里应优先用 == 做字符串比较以与 [[ 一致。

参考答案

  1. 错([[ 是 Bash 关键字)。
  2. 错(右侧有引号,比的是字面 *.log)。
  3. 对。
  4. 对。
  5. 错([ 里用 ===[ 中不可靠)。

实操题 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.logrelease-*.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-v1api-)。
  • B :正则,整体须为 api- + 一串小写字母或数字(如 api-v2api-V2 不匹配)。

下一篇预告

S05-04 :《算术判断 (( ))let:数值比较与自增》--- 在条件里写 (( n > 0 ))((i++)),与 [[-eq 的分工。