"|| exit /b" 和 "|| exit /b !errorlevel!" 之间的区别

Chr*_*ood 12 powershell cmd batch-file

我们有一堆.bat构建脚本,这些脚本由基于 PowerShell 的 GitLab 运行器调用,这些运行器最近重构为:

program args
if !errorlevel! neq 0 exit /b !errorlevel!
Run Code Online (Sandbox Code Playgroud)

更简洁:

program args || exit /b
Run Code Online (Sandbox Code Playgroud)

今天我调查了一个构建作业,如果您查看错误日志,该作业显然失败了,但报告为成功。经过大量实验,我发现这种模式并不总是按预期工作:

program args || exit /b
Run Code Online (Sandbox Code Playgroud)

但是当前者没有时,这似乎确实有效:

program args || exit /b !errorlevel!
Run Code Online (Sandbox Code Playgroud)

我已经阅读了 SO question Windows batch exit option b with or without errorlevel和下面来自https://www.robvanderwoude.com/exit.php的声明,但仍然不能完全解释我正在观察的内容。

DOS 联机帮助 (HELP EXIT) 没有明确说明 /B 参数退出当前脚本实例不一定与退出当前脚本相同。即,如果脚本在 CALLed 代码段中,则 EXIT /B 退出 CALL,而不是脚本。


这是我用来探索这个的最小批处理文件:

@echo off
setlocal EnableDelayedExpansion
cmd /c "exit 99" || exit /b
:: cmd /c "exit 99" || exit /b !errorlevel!
Run Code Online (Sandbox Code Playgroud)

这就是我调用批处理文件的方式(以模拟基于 GitLab PowerShell 的运行程序如何调用它):

& .\test.bat; $LastExitCode
Run Code Online (Sandbox Code Playgroud)

以下是根据批处理文件中两行中的哪一行执行的输出:

PS> & .\test.bat; $LastExitCode
0
PS> & .\test.bat; $LastExitCode
99
Run Code Online (Sandbox Code Playgroud)

还有另一种获得正确行为的方法,即也使用以下CALL方法从 PowerShell 中直接调用批处理文件:

PS> & cmd.exe /c "call .\test.bat"; $LastExitCode
99
Run Code Online (Sandbox Code Playgroud)

虽然我很欣赏这可能是从 PowerShell 调用批处理文件的正确方法,但根据我见过的许多示例,这似乎不是常识。我也想知道为什么 PowerShell 不以这种方式调用批处理文件,如果它是“正确的方式”。最后我仍然不明白为什么,当离开 时call,行为会根据我们是否将 加入!errorlevel!exit /b语句中而改变。


更新:感谢到目前为止的所有讨论,但我觉得它在杂草中迷失了,这可能是我的错,因为我的原始问题太含糊了。我认为我真正想要的是(如果可能的话)是关于何时(据称)在以下声明中评估的明确errorlevel声明:

program || exit /b
Run Code Online (Sandbox Code Playgroud)

是否真的program像这样早期评估(即在运行之前):

program || exit /b %errorlevel%
Run Code Online (Sandbox Code Playgroud)

或者它是懒惰地评估(即在运行exitprogram执行并且内部errorlevel已更新时),更类似于此:

program || exit /b !errorlevel!
Run Code Online (Sandbox Code Playgroud)

因此,我并不是真的在猜测,除非可悲的是,这是我们能做的最好的事情,在这种情况下,知道没有明确的答案或者这是一个错误对我来说是可以接受的答案:o)。

mkl*_*nt0 7

解决方法

  • 通过调用您的批处理文件cmd /c <batch-file> ... `& exit,在这种情况下,没有显式退出代码|| exit /b解决方案按预期工作。

    • cmd /c .\test.bat `& exit

      • 请注意`-escaped &,这会阻止 PowerShell预先解释&。略去`,如果你正在运行从一个环境中的命令不涉及一个外壳,如AA计划任务。
      • 或者,您可以使用 form cmd /c "<batch-file> ... & exit",但这需要转义"作为参数一部分的任何字符(或使用here-string)。如果没有使用 PowerShell 变量或表达式,单引号是一个选项,可以避免该问题:cmd /c '<batch-file> ... & exit'
    • 使用cmd /c <batch-file> ... `& exit 常规调用批处理文件,从外面cmd.exe是可取的,因为即使是批处理文件没有明确的exit /b(或exit)调用否则就表现异常-看到这个答案

  • 或者 - 但前提是你的批处理文件永远不需要从另一个应该返回控制的批处理文件中调用并且它永远不需要成为cmd /c 命令命令行的一部分,而它不是最后一个命令[1] -您可以使用|| exit代替|| exit /b- 这会立即作为一个整体退出执行cmd.exe过程,但是退出代码(错误级别)会可靠地报告(至少在语句的上下文中)也可以从外部直接调用,例如(或,在这个简单的例子中,只是) 来自 PowerShell。<command> || exitcmd.exe& .\test.bat.\test.bat

虽然setlocal EnableDelayedExpansionexit /b !ERRORLEVEL!作品结合(除了内部(...)- 请参阅这篇文章) - 由于使用了显式退出代码 - 它显然更麻烦并且可能产生副作用,特别是!从命令中悄悄删除字符echo hi!(虽然可以通过以下方式最小化该问题)在setlocal EnableDelayedExpansion呼叫之前将呼叫置于线路上exit /b,如果有多个出口点,则需要重复)。


cmd.exe在这种情况下,的行为是不幸的,但无法避免。

从外部调用批处理文件时cmd.exe

  • exit /b-没有退出代码(错误级别)参数 - 仅按预期设置cmd.exe 进程退出代码- 即批处理文件中最近执行的命令的退出代码-如果您跟随批处理文件调用& exit,即作为cmd /c <batch-file> ... `& exit

    • 没有& exit替代方法,一个参数少exit /b从一个批处理文件呼叫反映在%ERRORLEVEL%变量帧内cmd.exe-session,但这并不翻译成cmd.exe过程退出代码,然后默认0[1]

    • 随着& exit解决方法,内部批处理文件参数少exit /b 正确设置cmd.exe的退出代码,即使是在一个<command> || exit /b声明,在这种情况下<command>的退出代码被传递,如预期。

  • exit /b <code>关系,即通过退出代码<code> 明确地始终工作[2] ,即,& exit随后的解决方法不是必要的。

  • 这种区别是一个模糊的不一致,可以有理由地称为错误Jeb 的有用答案包含演示行为的示例代码(使用cmd /c call ...撰写本文时不太全面的解决方法,但它同样适用于cmd /c "... & exit")。


[1] 使用cmd /c,可以传递多条语句执行,最后一条语句决定cmd.exe进程的退出代码。例如,cmd /c "ver & dir nosuch"报告退出代码1,因为不存在的文件系统项nosuch导致dir将错误级别设置为1,无论前面的命令 ( ver) 是否成功。不一致之处在于,对于一​​个名为的批处理文件test.bat,它在exit /b 没有显式退出代码参数的情况下退出cmd /c test.bat总是报告0,而cmd /c test.bat `& exit正确报告在批处理文件退出之前执行最后一条语句的退出代码。

[2]的退出代码可以字面上或经由变量指定,但缺陷是-由于cmd.exe-的向上前变量扩展<command> || exit /b %ERRORLEVEL%确实工作如预期的,因为%ERRORLEVEL%在该点处膨胀到误差电平之前此发言, 不至于由<command>; 这就是为什么在这种情况下需要通过运行或使用选项调用延迟扩展的原因:setlocal enabledelayedexpansioncmd.exe/V<command> || exit /b !ERRORLEVEL!


jeb*_*jeb 6

exit /b和之间有区别exit /b <code>
正如 mklement0 所述,无论是否调用批处理文件,差异都变得可见CALL

在我的测试中,我曾经(call)将错误级别强制为 1。

测试1.bat

@echo off
(call)
exit /b
Run Code Online (Sandbox Code Playgroud)

测试2.bat

@echo off
(call)
exit /b %errorlevel%
Run Code Online (Sandbox Code Playgroud)

使用test-all.bat进行测试

cmd /c "test1.bat" & call echo      Test1 %%errorlevel%%
cmd /c "call test1.bat" & call echo call Test1 %%errorlevel%%
cmd /c "test2.bat" & call echo      Test2 %%errorlevel%%
cmd /c "call test2.bat" & call echo call Test2 %%errorlevel%%
Run Code Online (Sandbox Code Playgroud)

输出:

     测试 1 0  
调用 Test1 1  
     测试 2 1  
调用 Test2 1

要获得始终可靠的错误级别,您应该使用exit /b <code>.
在使用构造<command> || exit /b !errorlevel!的情况下,延迟扩展是必要的或形式

<command> || call exit /b %%errorlevel%%
Run Code Online (Sandbox Code Playgroud)

另一种解决方案

<command> || call :exit
...

:exit
(
   (goto) 2>nul
   exit /b
)    
Run Code Online (Sandbox Code Playgroud)

这使用批处理异常处理
Windows 批处理是否支持异常处理?


Mag*_*goo 5

让我们看看三种可能的情况:

\n
cmd /c "exit 99" || exit /b\n
Run Code Online (Sandbox Code Playgroud)\n

返回,0因为cmd /c "exit 99"执行正确

\n
cmd /c "exit 99" || exit /b !errorlevel!\n
Run Code Online (Sandbox Code Playgroud)\n

返回,99因为cmd /c "exit 99"设置errorlevel为 99 并且我们返回执行结果的错误级别cmd /c "exit 99"

\n
cmd /c "exit 99" || exit /b %errorlevel%\n
Run Code Online (Sandbox Code Playgroud)\n

返回?- 解析该cmd /c "exit 99"行时的错误级别,\n评估 `%errorlevel% 时的错误级别。

\n

如果delayedexpansion设置,则唯一的区别是!errorlevel!场景尝试将字符串分配给错误级别,这很可能不会很好地工作。

\n

至于 Powershell——它是人迹罕至的道路上的一个极端案例。.exe由于设计者期望使用此工具执行 s 等,因此未经过彻底测试的场景。毫无疑问,即使被举报,它也不会被修复,因为有一个解决方法,即使它没有得到很好的曝光。

\n

我相信,这就是“失败到失败”的场景——假设设施可以工作,因为很少满足导致其失败的正确条件。

\n

同样地,

\n
echo %%%%b\n
Run Code Online (Sandbox Code Playgroud)\n

是模棱两可的。它是指echo字面量%%b还是echo %元变量内容的前缀b?(答案:后者)。并不是每天都会遇到。模棱两可的问题最终得到解决的可能性有多大?我们甚至无法/u实现date命令以让它以通用格式传递日期,这将解决标签上发布的绝大多数面向日期的问题batch。至于切换到允许date交付自某个纪元日期以来的天数的可能性 - 我什至懒得提出建议,因为尽管邀请了cmd修改建议,但对所提供的设施绝对没有采取任何措施,只是用户-界面变化。当老信徒们在一个上锁的文件柜底部玩耍时,他们在玩所有闪亮的东西,而文件柜卡在一个废弃的厕所里,门上写着\xe2\x80\x98当心豹子。\xe2\x80\x9d

\n