本文详解 Bash 内建命令
shopt与nullglob选项:默认情况下,未匹配任何文件的通配符会以字面量字符串传入循环,导致脚本误执行甚至误删操作。开启 nullglob 后,无匹配的通配符直接展开为空。文章通过可复现实验对比两种行为,给出文件计数与批量处理的标准写法,并整理 shebang 兼容、test 存在性判断静默反转、set -u空数组报错等五个生产脚本踩坑点。
nullglob 选项拆解
shopt -s nullglob 由三部分组成:
| 组成部分 | 含义 |
|---|---|
shopt | shell options 的缩写,Bash 内建命令,用于开启或关闭 shell 行为选项 |
-s | set,启用指定选项;与之相对的 -u(unset)用于关闭选项 |
nullglob | 选项名:null(空)+ glob(通配符匹配),控制无匹配通配符的展开结果 |
默认行为的问题
未开启 nullglob 时,通配符若无任何匹配项,Bash 会把它当作普通字面量字符串保留下来:
# 假设当前目录没有任何 .txt 文件
for file in *.txt; do
echo "处理文件: $file"
done输出:
处理文件: *.txt循环执行了一次,*.txt 被当成了文件名。如果循环体内是 rm "$file" 之类的删除操作,就会得到 No such file or directory 报错。
更隐蔽的是利用通配符做存在性判断的场景。默认行为下无匹配时 [ -e *.txt ] 等价于 [ -e '*.txt' ],判断为假,勉强符合“没有文件”的直觉:
[ -e *.txt ] && echo "matched" || echo "not matched"
# 输出: not matched开启 nullglob 后
开启后,无匹配的通配符直接展开为空,循环列表为空,循环体一次都不执行:
shopt -s nullglob
for file in *.txt; do
echo "处理文件: $file"
done
echo "done"输出:
done这才是遍历场景通常期望的逻辑:没有匹配文件就不做任何事。
常见用法
结合数组可以安全地统计与批量处理文件:
shopt -s nullglob
files=(*.txt)
echo "共有 ${#files[@]} 个 txt 文件"
for f in "${files[@]}"; do
cp -- "$f" /backup/
done
# 恢复默认行为
shopt -u nullglob如果只在脚本的某个阶段启用,建议先保存状态再恢复,避免影响脚本其他位置的 glob 逻辑:
saved=$(shopt -p nullglob) # 记录当前状态,输出形如 shopt -u nullglob
shopt -s nullglob
# ... 需要空展开行为的处理逻辑 ...
eval "$saved" # 恢复原状态踩坑记录
1. 仅 Bash 支持,sh/dash 下直接报错
nullglob 是 Bash 扩展。Debian/Ubuntu 的 /bin/sh 指向 dash,Alpine 的 /bin/sh 是 ash,二者都没有 shopt,脚本会报 shopt: not found。shebang 必须写 #!/bin/bash。
需要 POSIX 兼容时,使用传统惯用法在循环体内自行过滤:
for f in *.txt; do
[ -e "$f" ] || continue
echo "处理文件: $f"
done2. 既有存在性判断会被静默反转
这是引入 nullglob 时最容易踩的坑。开启后 [ -e *.txt ] 在无匹配时展开为 [ -e ],而 [ 收到单个非空字符串参数时按“字符串非空”判定,结果为真:
$ shopt -s nullglob
$ ls *.txt
ls: *.txt: No such file or directory
$ [ -e *.txt ] && echo matched || echo not matched
matched明明没有文件,判断却变为真。旧脚本里依赖 [ -e *.txt ]、[ -f dir/*.cfg ] 这类判断的逻辑,在开启 nullglob 后必须改为基于数组计数:
files=(*.txt)
if [ "${#files[@]}" -gt 0 ]; then
echo "存在 txt 文件"
fi3. find * 等依赖位置参数的命令会破碎
find * -name x 在空目录下会展开为 find -name x,find 缺少起始路径,把 -name 当作路径解析后直接报错退出。类似的还有把 * 作为位置参数传给其他工具的写法,开启 nullglob 前需要逐一确认空展开后命令仍合法。
4. bash 4.4 之前 set -u 与空数组不兼容
set -u
shopt -s nullglob
files=(*.txt)
echo "${files[@]}" # bash < 4.4 报错: bash: files[@]: unbound variablebash 4.4 起,空数组在 set -u 下才允许展开。macOS 系统自带的 bash 3.2 仍会踩中。兼容写法是使用默认值展开或先判断计数:
echo "${files[@]-}" # 空数组时展开为空,不报错
[ "${#files[@]}" -gt 0 ] && printf '%s\n' "${files[@]}"5. 与 failglob 的取舍
nullglob 是“无匹配则跳过”,failglob 则是“无匹配直接报错”,适合希望暴露异常而不是静默处理的场景:
$ shopt -s failglob
$ for f in *.txt; do echo "$f"; done
bash: no match: *.txt小结
| 选项状态 | 无匹配通配符的行为 | 典型后果 |
|---|---|---|
| 默认 | 保留字面量字符串 | 循环误执行一次,文件操作报错 |
shopt -s nullglob | 展开为空 | 循环零次执行,符合遍历直觉 |
shopt -s failglob | 直接报错 | 暴露问题,不静默跳过 |
编写批量文件处理脚本时,建议在遍历前显式开启 nullglob 并用完即恢复;同时注意本文第 2、4 条坑,改造既有存在性判断与 set -u 场景后再引入。
参考
- https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
- https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
最近更新
最新评论