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

How to properly report an exit status in batch?

我遇到了一种奇怪的情况,我编写的批处理文件报告了不正确的退出状态。这是重现问题的最小示例:

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

现在奇怪的是,如果我删除 inside 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)不是一个选项,因为它会杀死整个解释器并阻止本地脚本的使用。

以下使用 CALL:

调用 bat 工作正常

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%

Exit Code is 1

哇!太怪异了!

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

D:\test>cmd /q /c bug.cmd
before
first if

D:\test>echo %errorlevel%
0

如果我将脚本重命名为 "bug.bat"

,我会得到相同的行为

如果删除第二个 IF,我也会得到预期的 return 代码 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 return 编辑为退出代码非常挑剔。

很容易显示批处理脚本 return 正在输入正确的 ERRORLEVEL,但 ERRORLEVEL 没有被 return 编辑为 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 被 returned 作为退出代码:

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

也许 simplest/safest 解决方法是将批处理脚本更改为 EXIT 1 而不是 EXIT /B 1。但这可能不切实际或不可取,具体取决于其他人如何使用该脚本。

编辑

我重新考虑过,现在认为这很可能是一个不幸的设计 "feature" 而不是一个错误。 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 的回答很好。我并不是要提出其他建议。但是,我发现为 return 代码和公共退出点使用变量是可靠的。是的,它需要一些额外的行,但也允许额外的清理,如果有必要,必须将其添加到每个出口点。

@ECHO OFF
SET EXITCODE=0

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

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

:TheEnd
EXIT /B %EXITCODE%

正如@dbenham 指出的那样,“[i]如果在同一命令块中 EXIT /B 之后解析命令,那么问题就会显现,即使后续命令永远不会执行”。在这种特殊情况下,IF 语句的主体基本上被评估为

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

其中 & 运算符是函数 cmd!eComSep(即命令分隔符)。 EXIT /B 1命令(函数cmd!eExit)通过将全局变量cmd!LastRetCode设置为1然后基本执行GOTO :EOF来评估。当它 returns 时,第二个 eComSep 看到 cmd!GotoFlag 已设置,因此跳过评估右侧。在这种情况下,它还会忽略左侧的 return 代码,取而代之的是 return 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 的 return 代码向上传递到堆栈,成为进程退出代码。

我将尝试加入 dbenham(从批处理代码检查案例)和 eryksum(直接转到代码)的答案。或许做的时候我能看懂。

让我们从 bug.cmd

开始
exit /b 1 & rem

从 eryksum 的答案和测试中我们知道这段代码会将 errorlevel 变量设置为 1,但命令的一般结果不是 失败 作为内部cmd 中的函数将处理连接运算符作为函数调用,该函数调用将 return (意味着 C 函数 returning 值)正确命令的结果。这可以测试为

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) returned SUCESS.

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

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

这里在前一个案例中进行条件执行 "fail" 的同一进程将 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 returns 执行代码的结果值。我们从之前的测试中知道这个值为 0(但 errorlevel / LastRetCode 为 1)。此值在 CallWork(三元 ? 运算符)内部进行测试:如果 BatProc return 值不为 0,则 return 值为 else return LastRetCode,在本例中为 1。然后此值在 eCall 中用作 return 值并存储在 LastRetCode 中(= 中的 return 命令是一个赋值)所以它被 returned in errorlevel.

如果我没有遗漏任何东西,其余情况只是相同行为的变体。