跳到正文

Glob 模式匹配:Shell、Git 与 Kubernetes 的规则差异 ​

一句话:glob 是用通配符匹配路径的简化语法,* 默认不跨目录、** 才递归;但 Shell、Git、Kubernetes 各自改了规则,混用就会得到「看起来该匹配却没匹配」的结果。

适用:bash 4.0+、Git 1.8.2+、kubectl 1.2x+。不同版本行为有差异,以各自官方文档为准。

为什么需要 ​

手写每个文件名不现实,正则表达路径又太重。glob 用最少的符号覆盖三类常见选择:按扩展名、按目录、按名字前缀。

关键区别在于谁在执行匹配:Shell 在把参数交给程序之前就展开 glob;Git、Kubernetes、构建工具则在自己的代码里匹配字符串。同一个模式在 ls 和 git 里结果不同,根源就在这里。

核心机制 ​

共同的三条规则 ​

  • * 匹配任意长度字符,但不匹配路径分隔符 /。
  • ? 匹配单个字符,同样不匹配 /。
  • [abc] 匹配字符组里的一个;[!abc] 取反(bash 也接受 [^abc])。
  • ** 表示递归,但不是所有工具默认开启。

Shell(bash / zsh) ​

  • * 不跨 /,也不匹配以 . 开头的隐藏文件。
  • ** 需要 shopt -s globstar(bash 4.0+);zsh 的 **/ 默认递归。
  • {a,b} 是花括号展开(brace expansion),在 glob 之前执行,属于 bash/zsh 扩展,不是 POSIX;dash 不支持。
  • 没有匹配时,默认把模式原样传给程序;shopt -s nullglob 才展开为空,failglob 直接报错。
  • 引号会阻止展开:"*.md" 是字面量。

Git(.gitignore / .gitattributes) ​

Git 的规则不是 shell glob,容易记反:

  • * 匹配任意字符,但不含 /;不含 / 的模式在任意深度匹配。*.log 会忽略所有目录下的 .log,不需要 **/。
  • 含 / 的模式相对 .gitignore 所在目录锚定;开头 / 锚定仓库根。
  • 结尾 / 只匹配目录,例如 build/。
  • **/ 匹配任意深度(包括零层);/** 匹配目录下所有内容;/**/ 匹配零或多层目录;其他连续 * 按普通 * 处理。
  • ! 取反,但如果父目录已经被排除,不能把子文件重新包含回来。
  • 验证命令是 git check-ignore -v <path>,它会打印命中的规则和来源文件。

Kubernetes ​

Kubernetes 的资源名是扁平字符串,没有文件路径,也没有 glob:

  • kubectl get pods myapp-* 不会由 kubectl 展开。在 shell 里它会被当成当前目录的文件名:没有匹配时(zsh 或开启 nullglob)直接报错,有匹配时会把本地文件名当成资源名。这是最常见的误用。
  • 选择资源用标签选择器(label selector):-l 'app=myapp'、-l 'app in (nginx,redis)'、-l '!canary';逗号表示 AND。
  • 字段选择器:--field-selector status.phase=Running,spec.nodeName=node-1;支持哪些字段随资源类型而定。
  • 确实要按名字前缀批量操作时,先列出来再删:kubectl get pods -o name | grep '^pod/myapp-' | xargs -r kubectl delete(-r 是 GNU 扩展,BSD/macOS 上要先判断输出非空)。
  • RBAC 的 resources: ["*"]、verbs: ["*"] 是通配,但 resourceNames 不支持通配。
  • Ingress 的 host: "*.example.com" 是 DNS 通配,只匹配一级子域;路径匹配由 pathType: Prefix/Exact/ImplementationSpecific 决定,不是 glob。
  • Helm 的 .helmignore 也是 glob,但实现和边界与 .gitignore 不同,以官方文档为准。

对照表 ​

场景* 跨 /**花括号验证方式
bash 路径展开否需 globstar支持printf '%s\n' pattern
Git ignore否支持(1.8.2+)不支持git check-ignore -v
K8s 资源名不适用不适用不适用kubectl get pods -l
Helm ignore否以官方文档为准以官方文档为准看 helm template 渲染结果

最小上手 ​

Shell:

bash
mkdir -p /tmp/globdemo/src/main /tmp/globdemo/src/test
cd /tmp/globdemo
touch src/main/A.java src/test/B.java src/C.java
shopt -s globstar nullglob
printf '%s\n' src/*.java      # 只有 src/C.java
printf '%s\n' src/**/*.java   # 三个都匹配,** 可匹配零层目录

Git:

bash
git init -q
printf '*.log\n/build/\nsrc/**/*.java\n!src/main/A.java\n' > .gitignore
mkdir -p build logs && touch logs/app.log build/out.bin
git check-ignore -v src/main/A.java src/C.java src/test/B.java logs/app.log build/out.bin

期望:src/main/A.java 显示 !src/main/A.java(最后一条规则是取反,最终不忽略);src/C.java 和 src/test/B.java 都命中 src/**/*.java,前者证明 ** 可以匹配零层目录;logs/app.log 命中 *.log;build/out.bin 命中 /build/。-v 会打印最后命中的规则,包括取反规则;去掉 -v 才只输出最终被忽略的路径。被跟踪的文件不受 .gitignore 影响,要先 git rm --cached。

Kubernetes(需要可达集群):

bash
kubectl get pods -l app=myapp -o name
kubectl get pods --field-selector status.phase=Running -o name | head

附:选完文件,再按模式筛内容 ​

按 glob 选完文件后,常要再按内容筛行或改文本。注意 grep --include 的模式是 glob,grep -E 的模式是正则,两种语法不要混:

bash
grep -rn --include='*.java' 'TODO' src/     # 文件用 glob 选,内容用固定串匹配
grep -E 'ERROR|FATAL' app.log | wc -l       # -E 后面是正则
awk -F',' '$3 > 100 {print $1, $3}' data.csv
awk '{count[$1]++} END {for (k in count) print k, count[k]}' access.log
sed -n '5,10p' app.log                      # 按行号取
sed -i.bak 's/old/new/g' file.txt           # 原地替换并留备份(GNU sed)
find src -name '*.java' -mtime -1 -print    # 按名字 glob + 修改时间选文件

sed -i 在 GNU sed 和 BSD/macOS sed 上参数不同,macOS 要写 sed -i ''。

生产动作 ​

  • 脚本开头用 set -euo pipefail 加 shopt -s nullglob,避免把未展开的模式当文件名传给 rm、mv。
  • 批量删除前先打印展开结果:printf '%s\n' pattern 或 find ... -print,确认后再执行;不要直接 rm -rf $pattern。
  • 变量拼进 glob 时,加引号会阻止展开;需要展开时不要加引号,并用 find -print0 | xargs -0 处理空格和换行文件名。
  • Git 规则不生效时先 git check-ignore -v,再看文件是否已被跟踪;跨目录匹配显式写 **,团队统一 Git 版本。
  • Kubernetes 批量操作走标签选择器,不依赖名字前缀;删除前用 -o name 预览;给资源打稳定的 app.kubernetes.io/name 标签。
  • .dockerignore、.helmignore、.gitignore 是不同实现,不要假设规则一致;用打包或渲染后的文件列表验证。

边界 ​

  • glob 不是正则:* 不表示「前一个字符重复」,. 没有特殊含义。复杂模式用正则,路径选择用 glob。
  • Shell 展开发生在命令执行前,程序内部匹配发生在之后;同名模式在不同工具里结果可能不同。
  • .gitignore 只影响未跟踪文件,已跟踪文件继续被跟踪;! 无法穿透已被排除的父目录。
  • Kubernetes 没有文件名 glob;资源名受 DNS 子域字符集和长度限制,不是任意字符串。
  • 隐藏文件、大小写、Unicode 规范化在不同文件系统和工具上行为不同;跨平台脚本要显式处理。
  • Helm/Docker 的 ignore 实现不是 POSIX glob,边界情况以各自文档为准。

讲清楚 ​

glob 解决的是「按模式选路径」,核心规则只有三条:* 不跨目录、? 匹配单字符、[...] 是字符组。真正的麻烦在于每个工具都做了自己的扩展:bash 的 ** 要开 globstar,花括号是 bash 扩展不是标准;Git 的 * 同样不跨 /,但不含 / 的模式会在任意深度匹配,**/ 可以匹配零层目录;Kubernetes 干脆没有路径 glob,资源名是扁平字符串,选资源要靠标签和字段选择器,kubectl get pod myapp-* 是典型误用。排查时不要猜:Shell 用 printf 看展开结果,Git 用 git check-ignore -v,K8s 用 -l 和 -o name 预览。生产脚本先开 nullglob、先打印再删除,能避免大部分「匹配为空却继续执行」的事故。

把一门技术讲透到既懂也会用