我面临一个奇怪的情况,我写的批处理文件报告错误的退出状态。这是一个重现问题的最小样本:
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
。
我无法理解造成这种情况的原因。嵌套if
语句是不合法的吗?
如何正确,可靠地发出批处理中的脚本退出状态?
注意:调用exit 1
(没有/b
)不是一个选项,因为它会杀死整个解释器并阻止本地脚本的使用。
答案 0 :(得分:5)
正如@dbenham指出的那样," [i] f在EXIT /B
之后解析一个命令,在同一个命令块内,然后问题就会出现,即使后续命令从不执行"。在这种特殊情况下,IF
语句的主体基本上被评估为
(echo first if) & (exit /b 1) & (if "" == "" (echo second if))
其中&
运算符是函数cmd!eComSep
(即命令分隔符)。通过将全局变量EXIT /B 1
设置为1然后基本上执行cmd!eExit
来评估cmd!LastRetCode
命令(函数GOTO :EOF
)。当它返回时,第二个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传递给堆栈以成为进程退出代码。
答案 1 :(得分:3)
哇!那怪异!
我能够通过运行以下命令从命令行控制台重现明显的错误(注意我使用/Q
关闭ECHO以便输出更简单):
D:\test>cmd /q /c bug.cmd
before
first if
D:\test>echo %errorlevel%
0
如果我将脚本重命名为&#34; bug.bat&#34;
,我会得到相同的行为如果删除第二个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.
我认为普通人(包括我自己)通常不区分这两者。但是,当批处理ERRORLEVEL作为退出代码返回时,CMD.EXE似乎非常挑剔。
很容易证明批处理脚本返回正确的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显式EXIT:
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
。但这可能不实际或不可取,这取决于其他人如何使用该脚本。
<强> 修改 强>
我已经重新考虑了,现在认为这很可能是一个不幸的设计和#34;功能&#34;而不是一个bug。 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
答案 2 :(得分:2)
我将尝试加入dbenham(检查批处理代码中的案例)和eryksum(直接转到代码中)的答案。也许这样做我能理解它。
让我们从bug.cmd
exit /b 1 & rem
从eryksum答案和测试我们知道这段代码会将errorlevel
变量设置为1,但命令的一般结果不是失败作为{{1内部的内部函数将串联运算符作为函数调用进行处理,该函数调用将返回(表示返回值的C函数)右命令的结果。这可以作为
cmd
是的,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
为1但条件执行将在errorlevel
之后运行代码,因为上一个命令(&&
)返回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:>
的编码类似于(粗略的汇编程序到C)
cmd
也就是说,function CallWork(){
....
ret = BatProc( whatIsCalled )
return ret ? ret : LastRetCode
}
function eCall(){
....
return LastRetCode = CallWork( ... )
}
命令在函数call
中处理,调用eCall
将上下文生成和执行委托给CallWork
。 BatProc
返回执行代码的结果值。我们从之前的测试中知道该值为0(但BatProc
为1)。此值在errorlevel / LastRetCode
(三元CallWork
运算符)中进行测试:如果?
返回值不为0,则返回值else,返回BatProc
,在这种情况下为1.然后在LastRetCode
内使用此值作为返回值并存储在eCall
内(返回命令中的LastRetCode
是一个yignation),因此它在=
中返回。
如果我没有遗漏某些内容,其余案例只是对同一行为的变异。
答案 3 :(得分:1)
以下工作正常,可以通过 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
答案 4 :(得分:1)
@ECHO OFF
SET EXITCODE=0
if "" == "" (
echo first if
set EXITCODE=%ERRORLEVEL%
GOTO TheEnd
if "" == "" (
echo second if
)
)
:TheEnd
EXIT /B %EXITCODE%