`getopt-example.bash` 中何时会触发 `Internal error!`?
正常使用这份示例时,只要 util-linux getopt 按预期生成带 shell 引号的结果,eval set -- "$TEMP" 成功,且选项声明与 case 分支一致,就无法仅靠命令行参数触发 Internal error!。这个分支用于捕获脚本内部的意外状态。
为什么正常输入不会进入这个分支?
case 处理的是 getopt 整理后的参数。进入循环前,原始输入已经经过解析:
TEMP=$(getopt -o 'ab:c::' --long 'a-long,b-long:,c-long::' -n 'example.bash' -- "$@")
这行声明了三组选项:
-a、--a-long:不带参数。-b、--b-long:必须带参数。-c、--c-long:可以带参数。
在示例预期的工作模式下,getopt 会整理选项,供循环逐项处理,并用 -- 标记选项部分的结束。接着,eval set -- "$TEMP" 用整理后的结果重新设置位置参数。
循环中的 shift 数量与选项声明对应:-a 消耗一个位置参数,-b 和 -c 各消耗两个。如果没有为 -c 提供可选参数,示例所用的引用模式会生成一个空参数,因此 shift 2 仍然成立。
只要这些约定成立,每次循环时的 $1 都会匹配已列出的选项或 --,不会落入 *)。
未知选项和普通参数为什么也不会触发?
遇到未知选项,或者选项缺少必需参数时,getopt 会先返回非零状态。脚本随后在这里退出:
if [ $? -ne 0 ]; then
echo 'Terminating...' >&2
exit 1
fi
这些输入不会进入后面的解析循环。
普通位置参数位于 getopt 输出的 -- 之后。循环遇到 -- 就会执行 break,剩余参数由最后的 for arg 处理。因此,即使普通参数的内容比较特殊,也不应直接落入兜底分支。
什么情况下保留它会有用?
一个直接的例子是修改脚本时更新了选项声明,却遗漏了对应的处理分支。
假设将 getopt 调用改成下面这样,增加无参数选项 -d,但保持原来的 case 不变:
TEMP=$(getopt -o 'ab:c::d' --long 'a-long,b-long:,c-long::' -n 'example.bash' -- "$@")
然后运行:
bash ./getopt-example.bash -d
根据修改后的代码,getopt 会接受 -d,但循环没有处理它的分支,于是落入 *) 并输出:
Internal error!
这说明解析规则与处理逻辑已经不同步。类似地,误改 shift 的数量,可能使选项参数被当成下一个选项处理;替换 getopt 实现或破坏其输出的还原过程,也可能打破循环依赖的约定。是否会触发这个分支,需要结合实际修改和运行环境判断。
为什么要立即退出?
这个分支可以看作运行时断言:程序认为这里不应该出现其他值,一旦出现,就停止处理并报告异常。
如果删除兜底分支,而循环遇到未匹配的值,当前这段 while true 不会执行任何 shift 或 break,可能不断检查同一个 $1,形成死循环。
排查时,可以让错误信息更具体。示例明确使用 Bash,因此可以使用 Bash 的 printf %q:
*)
printf 'Internal error: unexpected argument %q\n' "$1" >&2
exit 1
;;
这样可以看到触发异常的参数值。
关于 eval 的安全顾虑
这段代码依赖 getopt 生成正确引用的 shell 文本,再通过 eval set -- "$TEMP" 恢复参数边界。仅凭出现 eval,不能断定必须手工过滤所有输入;同样,也不能据此认为可以安全地对任意字符串执行 eval。
安全性取决于实际执行的 getopt、输出模式,以及中间是否保留了正确的引用。示例中的 "$@" 和 "$TEMP" 都是这一过程的重要部分。Internal error! 是解析循环的兜底诊断,不承担输入安全检查的作用。
备注:内容仅供参考。
