DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Bash local 接收命令输出后,失败状态变成 0

Bash 函数用 local 接收命令替换时,声明成功会覆盖子命令失败状态。分开声明和赋值,立即保存状态,并处理部分输出及调用者的错误传播。

函数里用 local result="$(some_command)" 接收输出,内部命令明明失败了,紧接着查看 $? 却是 0。把 local 去掉后又能得到非零状态,这通常是声明命令覆盖了你想检查的状态。

Bash 的 local 是内建命令。它成功声明并赋值时返回零;命令替换中的程序失败,不会自动让 local 也失败。下面两段不是同一种命令:

result="$(false)"
local result="$(false)"

第一段是没有命令名的赋值,正常完成展开和重定向后,状态来自最后一个命令替换。第二段执行的命令是 local,状态来自 local 本身。第二段只能放在函数内;函数外使用 local 是另一项错误,不能拿来验证此现象。

把声明与赋值分开

在函数开头先声明变量,再把赋值放进显式判断。以下完整 Bash 示例使用固定字符串与返回码,不依赖外部服务:

#!/usr/bin/env bash

produce_value() {
    printf '%s\n' 'partial output'
    return 7
}

read_value() {
    local result rc
    if result="$(produce_value)"; then
        printf 'value=%s\n' "$result"
    else
        rc=$?
        printf 'producer failed: %s\n' "$rc" >&2
        return "$rc"
    fi
}

if read_value; then
    printf '%s\n' 'completed'
else
    rc=$?
    exit "$rc"
fi

这里的成功标准是失败分支保存到状态 7,并将其传给调用者。produce_value 虽然输出过内容,read_value 也不会把这份部分结果当成成功数据继续使用。实际项目把 produce_value 换成已有的命令,并明确哪些状态允许继续,哪些必须中止。

rc=$? 必须是 else 内第一条命令。先打印日志、声明另一个变量或执行检查,再读取 $?,得到的就可能是这些后续命令的状态。也不要写成 if ! result="$(some_command)"; then rc=$?:! 已经取反,失败分支中读到的不再是原来的失败码。

不靠 set -e 推测结果

只增加 set -e,不能让成功的 local 突然变成失败。声明和赋值分开后,独立赋值的非零状态能否使脚本退出,还取决于它是否位于 if、AND/OR 列表等例外上下文。

显式判断更适合这里的需求,但它也不是自动修复生产命令内部所有错误的开关。示例中的 produce_value 明确 return 7。如果换成一个多步函数,并且该函数只靠 set -e 中止,那么把函数调用放进条件上下文可能改变它的错误处理行为。应让该函数自己检查关键步骤、明确返回失败,不要依赖调用者猜测内部有没有失败。

输出内容也不能替代退出状态。程序可能成功但不输出内容,也可能输出部分内容后失败。不能用“变量是否为空”判断命令是否成功。

export 与 declare 也要检查声明边界

同样的风险可以出现在 export VALUE="$(some_command)" 或 declare VALUE="$(some_command)" 中:这些是有自己状态的内建命令。需要同时赋值和导出时,先检查赋值,再单独 export,并继续处理 export 自身可能发生的错误。

普通赋值也不是任意多个子任务的错误汇总器。一个赋值语句内含多个命令替换时,规则取最后执行的命令替换状态。对每个都必须成功的任务,应逐条赋值并检查,不能只检查最后一项。

另外,命令替换会删除输出末尾的换行;这一点与状态覆盖无关。需要逐字节保存输出的业务,应另选合适的文件或流接口,不能因为状态处理正确就认为输出也完全未变。

验证失败路径与调用者

先在隔离样本中覆盖成功且有输出、成功但空输出、失败且无输出、失败但有部分输出四种情况。核对函数返回值、调用脚本的最终状态,以及失败后是否仍执行了依赖结果的步骤。

本文针对 Bash 内建 local。可用 help local 与 help declare 查看当前解释器接口,不能用某个同名外部程序的手册推断行为。修改前保留原脚本;回退恢复原错误处理逻辑,但不会撤销此前已经完成的写入、提交或发送。重试之前仍须核对那些动作是否已经发生。