DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Bash 用 [ ! -a 文件 ] 判断不存在却总进入真分支

Bash 的三参数 test 优先把 -a 解析为逻辑与,使非空文件名不再接受存在性测试。改用 -e,并区分悬空链接和路径访问失败。

Bash 中 [ -a "$file" ] 能检查文件,前面加上 ! 后,[ ! -a "$file" ] 却仍然进入真分支。只要文件名字符串非空,换成不存在的路径也可能得到相同结果。这时问题不在文件缓存,而在 [ 对三个参数的解释顺序。

本文限定 Bash 内建的 test 和 [。这两个命令按参数数量解析表达式;不要将这里的结果推广到所有 shell,或直接套到 [[ ... ]] 的语法规则。

三个参数里,-a 先被当成“与”

在这个判断中,末尾的 ] 是结束标记,表达式有三个参数:

[ ! -a "$file" ]

它们依次是 !、-a 和文件名。Bash 对三个参数先检查第二项是否为二元运算符;-a 在这个位置算逻辑“与”。于是两侧变成两个单字符串测试:字符串 ! 非空,文件名也非空,整体就为真。

解析器并没有先把 ! 解释为否定,再对后面的 -a 做文件存在测试。因此,文件是否存在可以完全没有参与这次判断。若 file 是空字符串,右侧单字符串测试变假,结果又会变化;这种变化不能证明它检查了文件。

不带否定的 [ -a "$file" ] 只有两个参数,这时 -a 可以作为一元文件测试。正是同一个符号在不同参数结构里的两种用途,造成了“单独能用,否定后失效”。

用 -e 表达文件存在,用独立条件表达逻辑与

如果原意是“路径不存在”,改为:

if [ ! -e "$file" ]; then
    printf 'Path is absent or cannot be resolved: %s\n' "$file"
fi

-e 没有同名的二元逻辑“与”用途,这个三参数表达式会按否定文件测试解释。保留变量的双引号,使空字符串、空格或通配符字符仍然作为一个参数传入;去掉引号不会修复歧义,反而可能改变参数数量。

若要合并两个条件,使用两个独立测试及 shell 的 &&:

if [ -e "$file" ] && [ -r "$file" ]; then
    printf 'Path exists and is readable: %s\n' "$file"
fi

不要继续写成 [ -e "$file" -a -r "$file" ],避免多参数语法再遇到类似运算符歧义。确实需要普通文件时,另选 -f;-e 为真并不意味着它一定是普通文件。

用存在与不存在的路径分别验收

下面的脚本只输出判断结果,不创建、覆盖或删除目标文件。第一个参数应是当前账号能够访问的已有路径,第二个参数应是已确认不存在的路径:

#!/usr/bin/env bash
if [ "$#" -ne 2 ]; then
    printf 'Usage: %s EXISTING_PATH MISSING_PATH\n' "$0" >&2
    exit 2
fi

for file in "$@"; do
    if [ ! -e "$file" ]; then
        printf 'not-e=true: %s\n' "$file"
    else
        printf 'not-e=false: %s\n' "$file"
    fi
done

预期已有路径得到 not-e=false,不存在的路径得到 not-e=true。在原脚本中也要核对最终进入的分支,而不只是确认命令没有语法报错。若脚本由 Bash 之外的解释器执行,先明确实际调用入口,不要依赖当前终端的 $SHELL 推测。

不存在、悬空链接和访问失败仍需区分

Bash 的多数文件测试会跟随符号链接,-e 也一样。因此悬空符号链接的目标不存在时,-e 为假,但链接本身仍在。需要判断路径入口是否为符号链接时使用 -L;需要区分链接与其目标时,把两项测试分别处理。

路径不可遍历、权限不足等情况也可能让存在性测试失败。不能把 [ ! -e "$file" ] 为真直接当作“可以安全创建或覆盖这个名字”的授权,更不能在此分支默认删文件。检查与后续打开之间,路径还可能被其他进程改变;真正的创建、读取操作必须自行处理失败并采用业务所需的原子性方案。

修改前保留原脚本,只替换已经确认意图的条件,不要全局把 -a 字符串替换成 -e:其他命令的选项及原本的二元表达式需要分别处理。回退可以恢复条件代码;若旧错误分支已经执行过写入或删除,恢复脚本本身无法撤销那些数据变更。