返回博客

set -u 下中文全角标点紧贴变量名导致「unbound variable」的诡异报错

构建脚本跑得好好的,某天加了一行中文提示信息,CI 突然挂了。报错只有一行:

build.sh: line 143: PLAYWRIGHT_BROWSERS_PATH�: unbound variable

明明前几行刚 export 过这个变量,怎么会 unbound?那个 (Unicode 替换符)更让人一头雾水,以为是文件编码损坏了。

现象

脚本开头有 set -euo pipefail,某行写了:

export PLAYWRIGHT_BROWSERS_PATH="$(pwd)/sidecar/.playwright"
# ... 若干行之后 ...
echo "❌ 找不到 Chromium.app($PLAYWRIGHT_BROWSERS_PATH)"

运行时在 echo 那行崩溃,错误信息里变量名后面跟着一个乱码替换字符

根因

bash 的变量名解析器依据 POSIX 规则,只把 ASCII 字符里特定的一批当作变量名的终止符:空格、"'/() 等。UTF-8 多字节字节序列(0x800xFF)不在这个终止符集合里

中文全角左括号 是 U+FF08,UTF-8 编码为三个字节 0xEF 0xBC 0x88。全角右括号 是 U+FF09,编码 0xEF 0xBC 0x89

当 bash 解析 $PLAYWRIGHT_BROWSERS_PATH) 时,它看到 $ 后面跟着 PL、……、H,然后是 0xEF,并不认识 0xEF 是"停止"的信号,于是继续贪婪地把后续字节也并入变量名:

变量名 = "PLAYWRIGHT_BROWSERS_PATH" + 0xEF + 0xBC + 0x89

这个带有高位字节的"变量名"从未被 export 过,所以 set -u 触发 unbound variable。终端在打印错误信息时无法渲染这三个孤立字节,于是显示为 (替换符)。

坑:全角标点贴着 $VAR 会被 bash 并入变量名

echo "...($VAR)..." 中,右括号 的 UTF-8 首字节 0xEF 被 bash 贪婪地当成变量名的一部分。set -u 下因为这个"新变量名"未定义而报错,且错误信息含 ,掩盖真实原因。

此坑在 CJK 输入法环境下格外易中:IME 默认产出全角标点,肉眼难以区分全角 和半角 (bash -n 语法检查也检测不出这个问题,因为它是运行时展开错误,不是语法错误。

正解

两种修法,任选其一:

# 方案一:用 ${} 显式界定变量边界
echo "❌ 找不到 Chromium.app(${PLAYWRIGHT_BROWSERS_PATH})"
 
# 方案二:将全角标点改为 ASCII 半角
echo "❌ 找不到 Chromium.app ($PLAYWRIGHT_BROWSERS_PATH)"

${VAR} 有效是因为 bash 把 } 视为无歧义的终止符,无论后面跟的是什么字节。

正解:在用户可见字符串里统一用 ${VAR} 形式

凡是在 echo / printf / heredoc 等面向人类的字符串里引用变量,一律写成 ${VAR},而不是裸 $VAR。这样无论后面紧跟什么标点,bash 都不会把它并进变量名。

全项目扫描

已经有脚本的仓库可以用下面的 grep 一次性定位所有危险写法:

grep -rnP '\$[A-Za-z_][A-Za-z0-9_]*[^A-Za-z0-9_$"\x27/{}\[\] \t\n.,:;|&<>()=+*\\!?~`@#%^-]' \
  scripts/

匹配逻辑:$IDENTIFIER 后面紧跟一个"非安全 ASCII 分隔符"的字符,也就是高位 UTF-8 字节的落点。

一句话外卖

在 shell 脚本的用户可见字符串里,永远用 ${VAR} 而不是裸 $VAR;中文全角标点不是 bash 的变量名终止符。