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

时间:2015-06-08 16:57:27

标签: python windows batch-file exit-code

我面临一个奇怪的情况,我写的批处理文件报告错误的退出状态。这是一个重现问题的最小样本:

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)不是一个选项,因为它会杀死整个解释器并阻止本地脚本的使用。

5 个答案:

答案 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将上下文生成和执行委托给CallWorkBatProc返回执行代码的结果值。我们从之前的测试中知道该值为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)

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

@ECHO OFF
SET EXITCODE=0

if "" == "" (
        echo first if
        set EXITCODE=%ERRORLEVEL%
        GOTO TheEnd

        if "" == "" (
                echo second if
        )
)

:TheEnd
EXIT /B %EXITCODE%