Bash 命令中的通配符通常会在目标程序启动前变成一组路径。它让批量处理很方便,也让同一行命令受到当前目录和 shell 选项的影响。程序拿到的是已经形成的参数,不会收到标记来告诉它哪些参数来自星号。因此,写命令时应先确定:希望传入实际匹配的文件,还是希望把模式本身交给另一个程序解释。两者需要不同的引号写法,不能依靠目标程序猜测意图。
先匹配,再保留参数边界
Bash 先进行分词,再进行文件名展开。没有引用的星号、问号和方括号模式可能因此生成多个参数。但匹配到的文件名即使包含空格,也仍然是一个参数,因为文件名展开之后不会再执行一次分词。常见问题发生在下一步:循环拿到了完整路径,却用没有引号的变量把它传给另一个命令。循环体中的 "$file" 仍然必要,不能因为前一步通配成功就省掉。
如果接收方需要模式,就应引用模式。例如 find . -name '*.txt' 将匹配规则交给 find;去掉引号后,Bash 会先在当前目录展开它,find 得到的可能是多个文件名,已经不是原来要表达的条件。变量里包含星号时也一样:引用变量会传递字面星号,不引用则可能触发文件名展开。引号只控制当前 shell 如何形成参数,不会禁止接收方按自己的语法解释收到的文本。
通配符匹配目录项,并不保证结果都是普通文件。名为 archive.txt 的目录也能匹配 *.txt,只想处理普通文件时还需要判断类型。普通星号不会越过斜杠,默认也不匹配名字开头的点;需要隐藏文件时,应明确使用相应模式或开启 dotglob。遍历整棵目录树是另一个需求,不能把当前目录的一次展开误认为递归搜索。
环境变量也能改变结果集合。Bash 中非空的 GLOBIGNORE 会过滤匹配结果,并且产生开启 dotglob 的副作用。因此,从交互会话继承状态的脚本可能意外纳入隐藏文件。下面的实验会清除 GLOBIGNORE 及较新版本的排序覆盖变量 GLOBSORT,并明确指定相关选项,让断言对应一套可重复的环境,而不是依赖读者终端的偶然配置。
坏掉的样子
默认情况下,没有匹配结果的模式会保留为原文。因此,遍历 *.png 的循环可能仍然运行一次,变量内容就是一个并不存在的星号文件名。开启 nullglob 后,未匹配的模式会直接消失,但这也可能让接收方变成零文件参数调用。某些程序此时等待标准输入,另一些程序则使用默认目标。未定义变量检查和管道失败检查都不能证明通配符至少找到了一个文件。
对于允许为空的输入,可以在开启 nullglob 后先收集到 Bash 数组,检查元素数量,再决定是否调用消费者。对于必须存在的输入,failglob 会在未匹配时报告展开错误,并阻止该命令执行。如果只想临时改变行为,把选项修改放在子 shell 中,避免影响调用者的后续命令。交互终端的启动文件可能已经修改选项,所以终端上一次成功不能替代脚本自己的前置条件。
以连字符开头的文件名还有另一类风险:接收程序可能将它当作选项。使用 ./*.txt 给结果添加目录前缀,能够避免这种名字直接位于参数开头;程序支持时,还应使用其文档规定的 -- 终止选项解析。引号保留参数边界,却不会关闭目标程序的选项解析。枚举和使用文件也不是原子操作,匹配之后文件仍可能被删除或替换,后续读取必须按照实际结果处理失败。
快速验证
下面的完整代码块需要 Bash 和常见临时目录工具。它自行创建测试目录,分别验证空格文件名、字面模式、未匹配策略和隐藏文件,退出时只删除自己的测试目录。不需要提前准备文件,也不会对当前目录已有文本文件执行操作。
bash --noprofile --norc <<'BASH'
set -e
export LC_ALL=C
set +f
unset GLOBIGNORE GLOBSORT
shopt -u nullglob failglob dotglob
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
cd "$work"
touch -- a.txt 'two words.txt' .hidden.txt -option.txt
files=(./*.txt)
[[ ${#files[@]} -eq 3 ]]
[[ ${files[2]} == './two words.txt' ]]
printf 'match <%s>\n' "${files[@]}"
literal=('*.txt')
[[ ${literal[0]} == '*.txt' ]]
missing=(./*.missing)
[[ ${missing[0]} == './*.missing' ]]
shopt -s nullglob
missing=(./*.missing)
[[ ${#missing[@]} -eq 0 ]]
if (shopt -s failglob; printf '%s\n' ./*.missing) 2>/dev/null; then
printf 'unexpected match\n' >&2
exit 1
fi
shopt -s dotglob
files=(./*.txt)
[[ ${#files[@]} -eq 4 ]]
printf 'PASS: spaces, literal, no-match policies, hidden file\n'
BASH
预期先输出三行匹配结果,其中含空格的文件名完整地处于一行,最后输出通过信息。最初数组没有隐藏文件,开启 dotglob 后才变成四项。触发 failglob 的子 shell 失败属于预期结果,不能执行到打印文件名的命令;如果它反而成功,外层会明确报错退出。这些断言检查的是 Bash 展开和参数保留行为,不能代替接收程序对零参数、目录或选项的语义验证。把示例中的无害打印替换成真实操作前,还应检查该程序的输入契约。
来源
- GNU Bash:文件名展开,匹配规则和未匹配选项,已核验 2026-09-06。
- GNU Bash:Shell 展开,展开顺序与参数数量变化,已核验 2026-09-06。
- GNU Bash:数组,收集匹配结果与保留元素边界,已核验 2026-09-06。