主题
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、先打印再删除,能避免大部分「匹配为空却继续执行」的事故。