#!/usr/bin/env bash # # bash 3.2(macOS 自带的 /bin/bash)兼容性检查。 # # ## 为什么要有它 # # 彩排 `dump` 时连中两元,两条都**只在 macOS 的 bash 3.2 上现形**, # 在 Linux(bash 4/5)上、以及 `bash -n` 语法检查下**全是绿的**: # # ① `mapfile` —— 4.0 才有的内建。3.2 上直接 `mapfile: command not found` # ② `$VAR` 后面紧跟中文/全角字符 —— 3.2 把多字节字符**吞进变量名**: # `"SNAPSHOT=$SNAPSHOT)"` 变成去读一个名叫 `SNAPSHOT)` 的变量。 # `set -u` 下报错还算好;`set +u` 下更坏——静默取到空值,**不报错**。 # # 本目录的脚本满屏中文提示,第 ② 类几乎必然会踩。而这类问题的代价不对称: # 自测绿 ≠ 能跑,等搬到第 300 张表才炸是最贵的失败方式。 # # ## 用法 # # bash sql/migration/tests/shell_compat_test.sh # # `10_dump_legacy.sh dump` 会在动第一张表之前先跑它,不通就中止。 # # ⚠️ 本文件**没有把自己列进 SCRIPTS**:它自己就含 `mapfile` 这个词(在 ① 的 grep 模式里), # 自扫会误报。代价是它的裸 `$VAR`+全角字符没人静态检查——但由 C 段兜住: # 本文件每行都会被真的执行,写错就当场 unbound variable(这个 bug 就是在自己身上抓到的)。 # # ⚠️ 别把这里的检查当成"字符集自测"的重复——那个测的是**改写结果对不对**, # 这个测的是**脚本本身跑不跑得起来**,两件事。 set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPTS=("$HERE/../10_dump_legacy.sh" "$HERE/charset_rewrite_test.sh" "$HERE/97_negative_test.sh") FAIL=0 ok() { printf ' ✅ %s\n' "$1"; } bad() { printf ' ❌ %s\n' "$1"; FAIL=1; } echo "A. 当前 bash 版本" V="$("${BASH:-bash}" --version | head -1)" printf ' ℹ️ %s\n' "$V" # 只在 3.2 下跑才是"真验证";但检查项本身在任何版本下都成立(加花括号、不用 mapfile) # 所以不做版本分支,永远跑。 echo "B. 静态检查(在 bash 4/5 上也能跑,防的是「换台机器就炸」)" for f in "${SCRIPTS[@]}"; do [[ -f "$f" ]] || { bad "找不到 $f"; continue; } name="$(basename "$f")" # ① bash 4+ 专有内建 / 语法 hits="$(grep -nE '\b(mapfile|readarray)\b|declare[[:space:]]+-A|\$\{[A-Za-z_][A-Za-z0-9_]*(\^\^|,,)' "$f" \ | grep -vE '^[0-9]+:[[:space:]]*#' || true)" if [[ -z "$hits" ]]; then ok "${name}:无 bash 4+ 专有语法" else bad "${name}:用了 bash 4+ 专有语法(3.2 上没有)"; printf '%s\n' "$hits" | sed 's/^/ /'; fi # ② `$VAR` 后紧跟非 ASCII(多字节会被吞进变量名)。用 python 判,别用 grep: # BSD grep 的字符类在 locale 下对多字节字符的边界判定不可靠。 hits="$(python3 -I - "$f" <<'PY' import io, re, sys path = sys.argv[1] for i, line in enumerate(io.open(path, encoding="utf-8"), 1): if line.lstrip().startswith("#"): continue for m in re.finditer(r'\$([A-Za-z_][A-Za-z0-9_]*)([^\x00-\x7f])', line): print("%s:%d 裸 $%s 后面紧跟 %r —— 改成 ${%s}" % (path, i, m.group(1), m.group(2), m.group(1))) PY )" if [[ -z "$hits" ]]; then ok "${name}:无「裸 \$VAR 后紧跟多字节字符」" else bad "${name}:多字节字符会被吞进变量名(bash 3.2 独有坑)"; printf '%s\n' "$hits" | sed 's/^/ /'; fi done echo "C. 真跑一遍(静态检查兜不住的那部分)" for f in "${SCRIPTS[@]}"; do [[ -f "$f" ]] || continue # 用 3.2 本体(/bin/bash)而不是 PATH 里可能存在的 bash 5 跑语法检查, # 这样"语法在 3.2 上成立吗"才是真的被验证过。 if /bin/bash -n "$f" 2>/dev/null; then ok "$(basename "$f"):/bin/bash -n 通过" else bad "$(basename "$f"):/bin/bash -n 失败($(/bin/bash --version | head -1 | cut -d, -f1))"; fi done echo if [[ "$FAIL" == 0 ]]; then echo "全部通过 ✅" else echo "有失败项 ❌ —— 这类问题在 Linux 上看不见,别因为「我这边能跑」就跳过" fi exit "$FAIL"