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 多字节字节序列(0x80–0xFF)不在这个终止符集合里。
中文全角左括号 ( 是 U+FF08,UTF-8 编码为三个字节 0xEF 0xBC 0x88。全角右括号 ) 是 U+FF09,编码 0xEF 0xBC 0x89。
当 bash 解析 $PLAYWRIGHT_BROWSERS_PATH) 时,它看到 $ 后面跟着 P、L、……、H,然后是 0xEF,并不认识 0xEF 是"停止"的信号,于是继续贪婪地把后续字节也并入变量名:
变量名 = "PLAYWRIGHT_BROWSERS_PATH" + 0xEF + 0xBC + 0x89
这个带有高位字节的"变量名"从未被 export 过,所以 set -u 触发 unbound variable。终端在打印错误信息时无法渲染这三个孤立字节,于是显示为 �(替换符)。
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 把 } 视为无歧义的终止符,无论后面跟的是什么字节。
凡是在 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 的变量名终止符。