shell_compat_test.sh 4.06 KB
#!/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"