Skip to content

错误处理矩阵:退出码、双轨制与异常

本页结论:八个运行体的错误模型分四族——bash/zsh 靠退出码 + set -e、fish 靠 $status(且永不自动中止)、cmd 靠 ERRORLEVEL + 延迟展开(没有任何自动中止)、PowerShell 5/7 靠 $ErrorActionPreference + try/catch$LASTEXITCODE 的双轨制,python 是异常。任务 07 的契约行 caughtError=trueafterFailure=continuedsetEExitCode=1scriptExitCode=0 八体一致,证明「失败可被捕获、捕获后照常继续」在所有模型里都成立;任务 01 的快照还顺手证明了两件事:stderr 与 stdout 确实分流(Linux 采集只收 stdout、stderr 行缺席于快照;Windows 采集用 2>&1 合并、stderr 行被收回快照——一缺一有,互为镜像),以及退出码的数值由命令自身定义、不由 shell 定义(busybox ls 失败=1,GNU ls 失败=2,Windows dir 失败=1)。

统一实验

  • 任务 01:向 stdout/stderr 各写一行,再运行一个必失败的命令,取其退出码。
  • 任务 07:捕获一次失败并继续执行;再制造一次「未捕获即中止」的失败,取其退出码。

五个 Linux 运行体的任务 07 快照逐字一致:

text
# demos/bash/07_errors.sh.out.txt(demos/zsh、demos/fish、demos/pwsh、demos/python 快照逐字相同)
caughtError=true
afterFailure=continued
setEExitCode=1
scriptExitCode=0

Windows 侧三份任务 07 快照(demos/cmd/07_errors.bat.out.txtdemos/powershell5/07_errors.ps1.out.txtdemos/powershell7/07_errors.ps1.out.txt)与上图逐字一致——四行契约值没有任何平台浮动。

错误模型五维对照

运行体错误感知模型「遇错即停」开关错误恢复写法错误流分离子进程退出码获取
bash退出码(0 成功 / 非零失败),$? 取上一条set -e(本任务在子 shell 里演示)cmd || 恢复if ! cmd>&2 写 stderr、2>&1/2>/dev/null 重定向$?
zsh退出码 + $?,同 bashset -e,同 bash同 bash同 bash$?
fish$status(每条命令后刷新)没有等价开关:失败永不自动中止脚本if not cmd / cmd; or 恢复同 POSIX 重定向语法$status
pwsh(Linux)双轨:cmdlet 错误记录(可升级为终止性异常)+ 原生命令退出码$ErrorActionPreference='Stop'(把错误升级为终止性)try { } catch { }Write-Error(错误流)/ [Console]::Error.WriteLine(真 stderr)/ 2> $null$LASTEXITCODE
cmdERRORLEVEL(整数)没有:逐条手工 if !ERRORLEVEL! neq 0 检查if !ERRORLEVEL! neq 0 set "CAUGHT=true"1>&2 写 stderr、2>nul 丢弃cmd /c 后读 !ERRORLEVEL!
powershell5双轨,同 pwsh同 pwshtry/catch同 pwsh$LASTEXITCODE
powershell7双轨,同 pwsh同 pwshtry/catch同 pwsh$LASTEXITCODE
python异常Exception 体系)未捕获异常即中止(退出码 1)try/exceptprint(..., file=sys.stderr)subprocess.run(...).returncode

逐维度证据

捕获失败并继续:caughtError=trueafterFailure=continued

bash/zsh 用 || 接住失败(摘自 demos/bash/07_errors.sh):

bash
false || caughtError=true  # failure is caught; the script keeps running

fish 没有 set -e 这类开关,失败本来就不中止脚本,恢复用 if not(摘自 demos/fish/07_errors.fish):

fish
function run_fails
    return 3
end
if not run_fails
    set caught true
end

pwsh 必须把错误升级为终止性才进得了 catch(摘自 demos/pwsh/07_errors.ps1):

powershell
try {
    Get-Content -Path '/nonexistent-hello-shell' -ErrorAction Stop | Out-Null
} catch {
    $caughtError = $true
}

cmd 逐条检查 ERRORLEVEL,注意必须开延迟展开才能在块内读到刷新后的值(摘自 demos/cmd/07_errors.bat,快照实测 caughtError=trueafterFailure=continued):

bat
setlocal EnableDelayedExpansion
dir "%FIXTURES%\no-such-dir" >nul 2>nul
if !ERRORLEVEL! neq 0 set "CAUGHT=true"

python 用 check=True 把非零退出码变成异常再捕获(摘自 demos/python/07_errors.py):

python
try:
    subprocess.run([sys.executable, "-c", "raise RuntimeError('boom')"], check=True, ...)
except subprocess.CalledProcessError as exc:
    caught = True
    fail_code = exc.returncode

五条路径,同一行 caughtError=true + afterFailure=continued

「遇错即停」:各有各的开关,fish/cmd 干脆没有

bash/zsh 的 set -e 在子 shell 里演示:失败立刻中止该子 shell,退出码 1 被外层接住(摘自 demos/bash/07_errors.sh):

bash
( set -e; false )  # set -e makes the subshell stop at the first failure
setEExitCode=$?

快照行 setEExitCode=1 即此。pwsh 的对应物是子进程里的 $ErrorActionPreference='Stop':未捕获的终止性错误使子 pwsh 以非零码退出(摘自 demos/pwsh/07_errors.ps1):

powershell
& pwsh -NoProfile -Command '$ErrorActionPreference=''Stop''; Get-Content -Path /nonexistent-hello-shell | Out-Null' > $null 2> $null
Write-Output "setEExitCode=$LASTEXITCODE"

powershell7 与 pwsh 脚本逐字节相同(子进程同样调 pwsh),powershell5 同构但子进程改调 powershell& powershell -NoProfile -Command ...)——两份 Windows 快照均实测 setEExitCode=1。cmd 没有「遇错即停」,只能把失败命令放进子 cmd /c 再读 !ERRORLEVEL!(bat 源码注释原话:no set -e in cmd,快照实测同为 setEExitCode=1)。python 的「遇错即停」是语言默认:未捕获异常即退出码 1,本任务用 check=True 把它显式接住。

PowerShell 的双轨制

PowerShell 是唯一需要同时盯两条错误线的运行体:cmdlet 失败产生错误记录(默认不中止、不进 catch),原生命令失败只更新 $LASTEXITCODE。任务 01 各展示了一条轨:[Console]::Error.WriteLine 写 stderr,$LASTEXITCODE 收原生命令退出码(摘自 demos/pwsh/01_hello_io.ps1):

powershell
bash -c 'ls /nonexistent-hello-shell' 2>$null | Out-Null
Write-Output "childExitCode=$LASTEXITCODE"

错误流分离:Linux 快照缺席的 stderr 行,Windows 快照收回了它

任务 01 每个实现都向 stderr 写了一行(bash/zsh/fish:echo "stderr: ..." >&2;pwsh/powershell5/powershell7:[Console]::Error.WriteLine;cmd:echo ... 1>&2;python:print(..., file=sys.stderr))。两侧采集器对这行的处理恰好相反,从两个方向证明了分流:Linux 采集只收 stdout,因此五份 01 快照里都没有 stderr 那行;Windows 采集器(scripts/collect-windows.ps1)用 2>&1 把错误流并进输出,因此三份 Windows 01 快照里都有那行——它确实去了 stderr,才会被 2>&1 收回来:

text
# demos/bash/01_hello_io.sh.out.txt
stdout: hello from bash
childExitCode=1
scriptExitCode=0
text
# demos/cmd/01_hello_io.bat.out.txt(首行是 rem 注释回显;stderr 行被 2>&1 收回)
D:\a\hello-shell\hello-shell>rem 01_hello_io: stdout vs stderr streams and capturing a failing command exit code 
stdout: hello from cmd
stderr: this line goes to stderr 
childExitCode=1
scriptExitCode=0

(bash 快照里 stderr: this line goes to stderr>&2 送去了 stderr,不在快照内;cmd 快照里同一行因 2>&1 合并而现身。powershell5 快照同样含 stderr: this line goes to stderr;powershell7 脚本未加前缀,快照为 this line goes to stderr。)

退出码数值由命令定义,不由 shell 定义

对比任务 01 的 childExitCode= 行:bash/zsh/fish 三份快照是 childExitCode=1,pwsh/python 两份是 childExitCode=2。失败的都是同一条 ls,差别在容器里的实现:alpine 的 busybox ls 失败退出 1,debian 的 GNU ls 失败退出 2。Windows 三体则统一是 childExitCode=1——失败命令换成了 dir(cmd 直接跑、powershell5/7 经 cmd /c 跑),而 dir 的失败码就是 1。shell 只是如实转述命令给出的数值——这也是为什么跨 shell 脚本不该硬编码「失败码 = 几」,只该判断「是否非零」。

小结

结论证据
四族模型:退出码(bash/zsh)、$status(fish)、ERRORLEVEL(cmd)、双轨制(PS5/7),外加异常(python)任务 07 各实现源码与八体一致的契约行
「遇错即停」:bash/zsh set -e、PS $ErrorActionPreference='Stop'、python 默认即停;fish/cmd 无此设施setEExitCode=1 八体一致;cmd bat 源码注释 no set -e in cmd,快照实测同值
stderr 与 stdout 分流是普遍能力各实现的 >&2 / 1>&2 / [Console]::Error.WriteLine / file=sys.stderr;Linux 快照缺席 stderr 行,Windows 快照经 2>&1 收回该行,一缺一有互为镜像
退出码数值归命令所有任务 01 childExitCode=1(busybox ls、Windows dir)vs =2(GNU ls)

延伸阅读:错误与信号统一骨架矩阵总览实验说明

Released under the MIT License.