Watch at Ease看见世界,读懂生活。

Technology · 实用指南

Shell 报“bad substitution”?重写脚本前先证明解释器与语法族

固定真实启动方式,确认实际解析文件的解释器及版本,分类 expansion,再选择 Bash 或可移植 sh,并重放原路径。

原创示意图:把直接执行、显式解释器、source 与 wrapper 映射到真实 parser。
原创示意图:只有直接执行路径才由 shebang 选择解释器。 Watch at Ease

“bad substitution”并不证明表达式本身错误。合法 Bash expansion 也会因 sh、dash、旧 Bash、source 调用者或 wrapper 解析而失败;错误的 ${command ...} 即使在 Bash 中也会失败。

先保存命令和 stderr,再证明执行路径与有效解释器,按声明契约分类 token,只改一个分支,并用固定输入重放相同 wrapper 或 scheduler。

1. 固定启动路径与首个错误

记录完整命令、wrapper 或 scheduler 配置、cwd、环境变量名、文件路径、首条 stderr 与退出状态,并区分 ./script、bash script、sh script、source、pipe 或内嵌执行。

不要先改 /bin/sh、删 shebang 或批量替换 expansion;这些操作会掩盖选择错误 parser 的路径。

2. 证明实际解释器与功能级别

在受控复现中记录父子进程命令行、wrapper shell 设置与版本探针;$SHELL 不是当前 parser 的证据。

直接执行可执行脚本时 shebang 才生效;source、sh -c、hook、CI、cron 或 SSH wrapper 由调用者契约决定。

3. 分类表达式

把 token 与声明契约比对;普通 expansion 和常见前后缀删除可移植,replacement、substring、case、indirect 与 array 属于 Bash 扩展且版本可能不同。

${ls ...} 分类错误:${...} 是参数展开,$(...) 才是命令替换。不要用 eval 掩盖解析问题。

4. 选择一个明确契约

若项目要求 Bash,就让所有 wrapper、scheduler 与 CI 明确调用目标 Bash,并在需要新特性时增加版本检查。

若契约是 POSIX sh,就把 Bash-only 语法换成规范支持的写法;不要用 Bash 正文承诺任意 sh 都能解析。

5. 通过原启动器做解析与执行矩阵

用固定 fixture 在 dash -n、bash -n、生产 Bash 和真实 wrapper 下记录 path、version、status、stdout、stderr。

只有原路径和预期输出通过才验收;若 direct 与 explicit、execute 与 source 结果不同,应声明不同契约。

原创示意图:区分可移植参数展开、Bash 扩展、命令替换与验证。
原创示意图:分类 token、选择契约,再重放原启动器。 Watch at Ease