我正面临一个奇怪的情况,我写的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
中处理,以将上下文生成和执行委托给BatProc
。 BatProc
从执行代码返回结果值。 我们从前面的测试中知道这个值是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%