Bash 没匹配到文件:星号为什么会原样进入循环
空目录里的循环居然执行了一次
脚本用星号匹配当前目录的文本文件,目录里明明没有文件,循环却执行了一次,打印出带星号的名字。这是 Bash 默认路径名展开的行为:没有匹配结果时,模式原样保留。程序收到的不是“零个文件”,而是一个包含通配符文字的参数。
如果后续命令把这个参数当作输出路径,还可能创建一个名字真的带星号的文件。因此不能把循环有没有执行当作目录有无匹配文件的证据。先决定“没有文件”是正常空批次还是需要失败,再选择 nullglob 或 failglob,更容易写出可检查的脚本。
把三种行为放进同一个实验
下面保存为 Bash 脚本并用 bash 执行,适用于四点四及以上,本文在五点二验证,系统需提供 mktemp。脚本只创建临时目录,结束后自动清理。它先明确关闭相关展开选项并清除过滤变量,减少个人终端配置对结果的干扰。
默认模式用数组接住未匹配的文字;开启 nullglob 后,同样的模式得到空数组;failglob 则放进单独的子 Bash,验证展开失败时命令没有执行。最后建立普通文件、带空格文件和隐藏文件,检查数组展开保留名称边界,并展示隐藏文件默认不包含在内。
AI概念配图,非真实界面
#!/usr/bin/env bash
set -euo pipefail
set +f
unset GLOBIGNORE
shopt -u nullglob failglob dotglob nocaseglob
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
cd -- "$work"
files=(*.txt)
[[ ${#files[@]} == 1 && ${files[0]} == '*.txt' ]]
printf 'default: count=%s value=%s\n' "${#files[@]}" "${files[0]}"
shopt -s nullglob
files=(*.txt)
[[ ${#files[@]} == 0 ]]
printf 'nullglob: count=%s\n' "${#files[@]}"
if bash --noprofile --norc -O failglob -c ': *.txt; touch reached' \
>child.out 2>child.err; then
printf 'unexpected success\n' >&2
exit 1
else
rc=$?
[[ $rc -ne 0 && ! -e reached && -s child.err ]]
printf 'failglob: command did not run\n'
fi
touch -- 'one.txt' 'two words.txt' '.hidden.txt'
files=(*.txt)
[[ ${#files[@]} == 2 ]]
printf 'matched: count=%s\n' "${#files[@]}"
for file in "${files[@]}"; do
printf '<%s>\n' "$file"
done
shopt -s dotglob
files=(*.txt)
[[ ${#files[@]} == 3 ]]
printf 'with dotglob: count=%s\n' "${#files[@]}"
printf 'glob checks passed\n'空数组仍然需要业务判断
nullglob 让未匹配的模式从参数列表消失,所以数组长度可以准确表达当前匹配数量。但不要直接把空数组传给一个含义不明的命令:某些工具没有文件参数时会读取标准输入,另一些会采用默认目标。调用之前先处理零个结果,才能避免脚本在空批次时卡住。
数组赋值处故意保留未加引号的星号,让 Bash 完成路径名展开;使用结果时则给整个数组展开加双引号,使每个文件名保持一个参数。把星号本身加引号会阻止通配,不会解决空匹配问题。是否引用,取决于这一阶段究竟是在生成列表还是传递列表。
与用命令替换收集 ls 输出相比,直接让数组承接展开结果更容易保留空格与换行。它也不需要把显示格式重新解析成路径。本文只打印文件名作为演示;真正执行操作时,仍应使用工具支持的参数终止符,避免以连字符开头的名称被误当成选项。
failglob 是展开失败,不是命令自己失败
failglob 没找到匹配项时,Bash 在运行目标命令之前就报错。它不总能像普通命令返回非零那样放在同一层逻辑里处理,因此示例用独立子进程捕获失败,同时检查哨兵文件没有创建。实际脚本如果选择它,应该让调用层能够清楚看到整个任务失败。
不要同时开启多种选项后凭记忆猜结果。把匹配行为限制在专门的脚本或子 shell 内,有助于避免影响外层环境。示例中的选项只属于运行这个脚本的进程;如果把相同代码直接 source 到当前终端,选项与目录变化就可能留在交互环境中。
默认星号通常不会匹配以点开头的隐藏名称。开启 dotglob 可以改变这个范围,但应先确认隐藏文件是否属于本次工作,不能为了“看起来全了”就默认纳入。目录名也可能符合扩展名模式,若只允许普通文件,还应在遍历时按实际类型筛选。
最后,匹配结果只是某一时刻的目录观察,文件可能在真正打开前被移动或删除。数组解决的是参数边界与空集合语义,不保证并发环境中的文件始终存在。保留空目录、含空格、隐藏文件和处理中消失的样本,让操作失败也有明确的记录与退出状态。


