如何正确报告批量退出状态?

我正面临一个奇怪的情况,我写的batch file报告了一个不正确的退出状态。 这里是一个重现问题的最小样本:

bug.cmd

 echo before if "" == "" ( echo first if exit /b 1 if "" == "" ( echo second if ) ) echo after 

如果我运行这个脚本(使用Python,但实际上也是以其他方式启动时发生的问题),下面是我得到的:

 python -c "from subprocess import Popen as po; print 'exit status: %d' % po(['bug.cmd']).wait()" echo before before if "" == "" ( echo first if exit /b 1 if "" == "" (echo second if ) ) first if exit status: 0 

请注意exit status报告为0即使exit /b 1应使其为1

现在奇怪的是,如果我删除内部if子句(这应该不重要,因为exit /b 1之后的所有内容都不应该被执行),并尝试启动它:

ok.cmd

 echo before if "" == "" ( echo first if exit /b 1 ) echo after 

我再次启动它:

 python -c "from subprocess import Popen as po; print 'exit status: %d' % po(['ok.cmd']).wait()" echo before before (environment) F:\pf\mm_3.0.1\RendezVous\Services\Matchmaking>if "" == "" ( echo first if exit /b 1 ) first if exit status: 1 

现在exit status正确报告为1

我不知道是什么原因造成的。 嵌套陈述是否是非法的?

我怎样才能正确和可靠地发出我的脚本退出状态批量?

注意:调用exit 1 (不带/b )不是一个选项,因为它会杀死整个解释器并阻止本地脚本的使用。

正如@dbenham所指出的那样,“在同一个命令块中,在命令EXIT /B之后解析了fa命令,那么即使后续命令不执行,也会出现问题。 在这个特殊情况下, IF语句的主体基本上被评估为

 (echo first if) & (exit /b 1) & (if "" == "" (echo second if)) 

&运算符是函数cmd!eComSep (即命令分隔符)。 通过将全局变量cmd!LastRetCode为1,然后基本上执行GOTO :EOF来计算EXIT /B 1命令(函数cmd!eExit )。 当它返回时,第二个eComSep看到cmd!GotoFlag已设置,因此跳过评估右侧。 在这种情况下,它也忽略了左侧的返回码,而是返回SUCCESS (0)。 这被传递到堆栈成为进程退出代码。

下面我已经包含了运行bug.cmd和ok.cmd的调试会话。

bug.cmd:

 (test) C:\Temp>cdb -oxi ld python Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64 Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: python Symbol search path is: symsrv*symsrv.dll* C:\Symbols*http://msdl.microsoft.com/download/symbols Executable search path is: (1404.10b4): Break instruction exception - code 80000003 (first chance) ntdll!LdrpDoDebuggerBreak+0x30: 00000000`77848700 cc int 3 0:000> g Python 3.4.3 (v3.4.3:9b73f1c3e601, Feb 24 2015, 22:44:40) [MSC v.1600 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> from subprocess import Popen as po >>> po('bug.cmd').wait() Symbol search path is: symsrv*symsrv.dll* C:\Symbols*http://msdl.microsoft.com/download/symbols Executable search path is: (1818.1a90): Break instruction exception - code 80000003 (first chance) ntdll!LdrpDoDebuggerBreak+0x30: 00000000`77848700 cc int 3 1:005> bp cmd!eExit 1:005> g (test) C:\Temp>echo before before (test) C:\Temp>if "" == "" ( echo first if exit /b 1 if "" == "" (echo second if ) ) first if Breakpoint 0 hit cmd!eExit: 00000000`4a6e8288 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000000`002fed78=0000000000000000 1:005> kc Call Site cmd!eExit cmd!FindFixAndRun cmd!Dispatch cmd!eComSep cmd!Dispatch cmd!eComSep cmd!Dispatch cmd!Dispatch cmd!eIf cmd!Dispatch cmd!BatLoop cmd!BatProc cmd!ECWork cmd!ExtCom cmd!FindFixAndRun cmd!Dispatch cmd!main cmd!LUAGetUserType kernel32!BaseThreadInitThunk ntdll!RtlUserThreadStart 1:005> db cmd!GotoFlag l1 00000000`4a70e0c9 00 . 1:005> pt cmd!eExit+0xe1: 00000000`4a6e8371 c3 ret 1:005> r rax rax=0000000000000001 1:005> dd cmd!LastRetCode l1 00000000`4a70e188 00000001 1:005> db cmd!GotoFlag l1 00000000`4a70e0c9 01 . 1:005> gu;gu;gu cmd!eComSep+0x14: 00000000`4a6e6218 803daa7e020000 cmp byte ptr [cmd!GotoFlag (00000000`4a70e0c9)],0 ds:00000000`4a70e0c9=01 1:005> p cmd!eComSep+0x1b: 00000000`4a6e621f 0f85bd4d0100 jne cmd!eComSep+0x1d (00000000`4a6fafe2) [br=1] 1:005> cmd!eComSep+0x1d: 00000000`4a6fafe2 33c0 xor eax,eax 1:005> pt cmd!eComSep+0x31: 00000000`4a6e6235 c3 ret 1:005> r rax rax=0000000000000000 1:005> bp ntdll!RtlExitUserProcess 1:005> g Breakpoint 1 hit ntdll!RtlExitUserProcess: 00000000`777c3830 48895c2408 mov qword ptr [rsp+8],rbx ss:00000000`0029f6b0=00000000003e5638 1:005> r rcx rcx=0000000000000000 1:005> g ntdll!ZwTerminateProcess+0xa: 00000000`777ede7a c3 ret 1:005> g 0 

ok.cmd:

 >>> po('ok.cmd').wait() Symbol search path is: symsrv*symsrv.dll* C:\Symbols*http://msdl.microsoft.com/download/symbols Executable search path is: (ce4.b94): Break instruction exception - code 80000003 (first chance) ntdll!LdrpDoDebuggerBreak+0x30: 00000000`77848700 cc int 3 1:002> bp cmd!eExit 1:002> g (test) C:\Temp>echo before before (test) C:\Temp>if "" == "" ( echo first if exit /b 1 ) first if Breakpoint 0 hit cmd!eExit: 00000000`4a6e8288 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000000`0015e808=0000000000000000 1:002> kc Call Site cmd!eExit cmd!FindFixAndRun cmd!Dispatch cmd!eComSep cmd!Dispatch cmd!Dispatch cmd!eIf cmd!Dispatch cmd!BatLoop cmd!BatProc cmd!ECWork cmd!ExtCom cmd!FindFixAndRun cmd!Dispatch cmd!main cmd!LUAGetUserType kernel32!BaseThreadInitThunk ntdll!RtlUserThreadStart 1:002> gu;gu;gu cmd!eComSep+0x2c: 00000000`4a6e6230 4883c420 add rsp,20h 1:002> p cmd!eComSep+0x30: 00000000`4a6e6234 5b pop rbx 1:002> p cmd!eComSep+0x31: 00000000`4a6e6235 c3 ret 1:002> r rax rax=0000000000000001 1:002> bp ntdll!RtlExitUserProcess 1:002> g Breakpoint 1 hit ntdll!RtlExitUserProcess: 00000000`777c3830 48895c2408 mov qword ptr [rsp+8],rbx ss:00000000`0015f750=00000000002b5638 1:002> r rcx rcx=0000000000000001 1:002> g ntdll!ZwTerminateProcess+0xa: 00000000`777ede7a c3 ret 1:002> g 1 

在ok.cmd的情况下, cmd!eComSep只在堆栈跟踪中出现一次。 exit /b 1命令被评估为右侧操作数,所以看起来GotoFlag的代码从不运行。 相反,1的返回代码被传递到堆栈,成为进程退出代码。

哇! 这是怪异的!

我可以通过运行以下命令来重现命令行控制台中的明显错误(注意我使用/Q来关闭ECHO,因此输出更简单):

 D:\test>cmd /q /c bug.cmd before first if D:\test>echo %errorlevel% 0 

如果我将脚本重命名为“bug.bat”,我会得到相同的行为

如果我删除第二个IF,我也得到预期的返回码1。

我同意,这似乎是一个错误。 从逻辑上讲,我认为没有理由让这两个类似的脚本产生不同的结果。

我没有一个完整的解释,但我相信我理解的行为的一个重要组成部分:批次ERRORLEVEL和退出代码不指向相同的东西! 以下是EXIT命令的文档。 重要的一点是exitCode参数的描述。

 D:\test>exit /? Quits the CMD.EXE program (command interpreter) or the current batch script. EXIT [/B] [exitCode] /B specifies to exit the current batch script instead of CMD.EXE. If executed from outside a batch script, it will quit CMD.EXE exitCode specifies a numeric number. if /B is specified, sets ERRORLEVEL that number. If quitting CMD.EXE, sets the process exit code with that number. 

我认为普通人(包括我自己)通常不会区分这两者。 但是,CMD.EXE似乎是非常挑剔的批处理ERRORLEVEL作为退出代码返回。

很容易显示批处理脚本返回正确的ERRORLEVEL,但ERRORLEVEL不作为CMD退出码返回。 我显示ERRORLEVEL两次来证明显示它的行为没有清除ERRORLEVEL。

 D:\test>cmd /q /v:on /c "bug.cmd&echo !errorlevel!&echo !errorlevel!" before first if 1 1 D:\test>echo %errorlevel% 0 

正如其他人指出的那样,使用CALL会导致ERRORLEVEL作为退出代码返回:

 D:\test>cmd /q /c "call bug.cmd" before first if D:\test>echo %errorlevel% 1 

但是,如果在CALL之后执行另一个命令,则这不起作用

 D:\test>cmd /q /v:on /c "call bug.cmd&echo !errorlevel!" before first if 1 D:\test>echo %errorlevel% 0 

请注意,上述行为严格是CMD.EXE的函数,与脚本无关,如下所示:

 D:\test>cmd /q /v:on /c "cmd /c exit 1&echo !errorlevel!" 1 D:\test>echo %errorlevel% 0 

您可以在命令链末尾显式地使用ERRORLEVEL来退出:

 D:\test>cmd /q /v:on /c "call bug.cmd&echo !errorlevel!&exit !errorlevel!" before first if 1 D:\test>echo %errorlevel% 1 

这是没有延迟扩张的同样的事情:

 D:\test>cmd /q /c "call bug.cmd&call echo %errorlevel%&exit %errorlevel%" before first if 1 D:\test>echo %errorlevel% 1 

也许最简单/最安全的解决方法是将批处理脚本更改为EXIT 1而不是EXIT /B 1 。 但是这可能不实际,也不可取,取决于其他人如何使用脚本。

编辑

我已经重新考虑了,现在认为它很可能是一个不幸的设计“功能”,而不是一个错误。 IF声明是一个红鲱鱼。 如果一个命令在EXIT / B之后被解析,在同一个命令块中,那么即使后续命令从不执行,该问题也会出现。

test.bat的

 @exit /b 1 & echo NOT EXECUTED 

以下是一些测试运行,显示行为是相同的:

 D:\test>cmd /c test.bat D:\test>echo %errorlevel% 0 D:\test>cmd /c call test.bat D:\test>echo %errorlevel% 1 D:\test>cmd /v:on /c "call test.bat&echo !errorlevel!" 1 D:\test>echo %errorlevel% 0 

第二个命令是什么并不重要。 以下脚本显示相同的行为:

 @exit /b 1 & rem 

规则是,如果后面的命令会执行,如果EXIT / B是没有退出的东西,那么问题就体现出来了。

例如,这有问题:

 @exit /b 1 || rem 

但以下工作正常,没有任何问题。

 @exit /b 1 && rem 

这项工作也是如此

 @if 1==1 (exit /b 1) else rem 

我将尝试加入dbenham(从批处理代码检查案例)和eryksum(直接到代码)的答案。 也许这样做我可以理解它。

我们从一个bug.cmd开始

 exit /b 1 & rem 

从eryksum的答案和测试我们知道这个代码将errorlevel变量设置为1,但命令的一般结果不是一个失败,因为cmd内的内部函数将处理连接操作符作为函数调用将返回(意味着一个C函数返回值)右命令的结果。 这可以作为测试

 C:> bug.cmd C:> exit /b 1 & rem C:> echo %errorlevel% 1 C:> bug.cmd && echo NEL || echo EL C:> exit /b 1 & rem NEL C:> echo %errorlevel% 1 

是的,错误errorlevel是1,但条件执行将在&&前面的命令( eComSep )返回SUCESS之后运行代码。

现在,在一个单独的cmd实例中执行

 C:> cmd /c bug.cmd C:> exit /b 1 & rem C:> echo %errorlevel% 0 C:> 

这里,在前面的例子中,条件执行“失败”的过程将错误errorlevel 0传播出新的cmd实例。

但是,为什么call案件的工作?

 C:> cmd /c call bug.cmd C:> exit /b 1 & rem C:> echo %errorlevel% 1 C:> 

它的工作原理是因为cmd编码类似于(粗略汇编到C)

 function CallWork(){ .... ret = BatProc( whatIsCalled ) return ret ? ret : LastRetCode } function eCall(){ .... return LastRetCode = CallWork( ... ) } 

也就是说, call命令在调用CallWork函数eCall中处理,以将上下文生成和执行委托给BatProcBatProc从执行代码返回结果值。 我们从前面的测试中知道这个值是0(但是errorlevel / LastRetCode是1)。 此值在CallWork (三元操作符)内部测试:如果BatProc返回值不为0,则返回值else返回LastRetCode ,在这种情况下为1.然后在eCall内使用此值作为返回值AND存储在LastRetCode (在返回命令是一个asignation),所以它返回错误errorlevel

如果我没有错过任何东西,其余的情况只是相同的行为的变化。

以下工作正常使用CALL调用蝙蝠:

bug.bat:

 echo before if "" == "" ( echo first if exit /b 1 if "" == "" ( echo second if ) ) 

test.bat的:

 call bug.bat echo Exit Code is %ERRORLEVEL% 

退出代码是1

@ dbenham的答案是好的。 我不是在另外建议。 但是,我发现使用变量作为返回码和公共退出点是可靠的。 是的,它需要一些额外的行,但也允许额外的清理,如果有必要,将不得不被添加到每个退出点。

 @ECHO OFF SET EXITCODE=0 if "" == "" ( echo first if set EXITCODE=%ERRORLEVEL% GOTO TheEnd if "" == "" ( echo second if ) ) :TheEnd EXIT /B %EXITCODE%