【问题标题】:How to properly report an exit status in batch?如何正确批量上报退出状态?
【发布时间】:2015-06-08 16:57:27
【问题描述】:

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

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

【问题讨论】:

  • 就像一般实践中的注释一样,当您想要评估批处理脚本中的文字括号时,您应该双抽它们。我想知道外部批处理运行时是否将 %d' % 评估为 null,并为您的内联 Python 命令产生意想不到的后果。试试print 'exit status: %%d' %% po... 等,看看是否有什么不同。
  • @rojo:在这种情况下应该没关系:我尝试在 Python shell 中启动它,但同样的事情发生了。
  • 在没有 Python 的情况下无法重新创建。如果我在cmd 控制台中从您的第一个代码块运行 bug.cmd,echo %ERRORLEVEL% 会显示 1。您所说的“使用 Python,但在以其他方式启动时也会出现问题” ,还有什么方法?
  • @rojo 我们的构建机器表现出相同的行为,但不使用 Python 来启动脚本。我得问问他们到底用什么。话虽如此,subprocess 只是在后台调用CreateProcess,所以 Python 很可能不是问题。
  • @rojo:你是如何启动它的?如果我使用 cmd /C bug.cmd 然后调用 echo %errorlevel% 我会重现奇怪的行为。

标签: python windows batch-file exit-code


【解决方案1】:

正如@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。当它返回时,第二个 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 被向上传递到栈中成为进程退出码。

【讨论】:

  • 我认为还有更多。在这种情况下ver > nul & (( set /p= <nul & break ) && echo NEL || echo EL ) & if errorlevel 1 ( echo EL ) else ( echo NEL ) 错误级别由ver 清除,由set /p 引发,条件执行运算符未检测到,但if errorlevel 1 检测到
  • @MCND, set /p(即cmd!eSet)返回1(即失败)并将全局变量cmd!LastRetCode(即cmd!eErrorlevel测试的值)设置为1。然后& 运算符(即cmd!eComSep)的 RHS 调度 break。这将返回 SUCCESS,但不会修改 LastRetCode。由于 & 运算符成功,&& 运算符 (cmd!eAnd) 的 RHS 被评估为运行 echo NEL。
  • @MCND,您的示例与 OP 的情况有何关系?在 OP 的代码中,EXIT /B 1 的返回代码为 1,并将全局变量 cmd!LastRetCode 设置为 1。但其评估设置为 cmd!GotoFlag,这导致 & 运算符的 RHS 评估为short-电路并成功(但不修改LastRetCode),即cmd!eComSep返回0,将调用堆栈向上传递给cmd!main并设置为cmd.exe进程退出代码。
  • 谢谢,现在我看到了。
  • 如果我真的明白this应该是问题、你的答案和我的样本之间的关系。
【解决方案2】:

哇!太可怕了!

我可以通过运行以下命令从命令行控制台重现明显的错误(注意我使用/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

【讨论】:

    【解决方案3】:

    我将尝试加入来自 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 命令在函数eCall 中处理,该函数调用CallWork 将上下文生成和执行委托给BatProc。 BatProc 返回代码执行的结果值。我们从之前的测试中知道该值为 0(但 errorlevel / LastRetCode 为 1)。该值在CallWork(三元?运算符)内部测试:如果BatProc返回值不为0,则返回该值,否则返回LastRetCode,在这种情况下为1。然后该值在内部使用eCall 作为返回值并存储在LastRetCode 中(返回命令中的= 是一个赋值),因此它在errorlevel 中返回。

    如果我没有遗漏什么,其余情况只是相同行为的变体。

    【讨论】:

    • 这正确描述了call 的工作原理。这是来自 Windows 7 中 cmd!CallWork 的相关 x64 机器代码:call cmd!BatProc、mov ecx,dword ptr [cmd!LastRetCode]、cmp eax,r13d、cmovne ecx,eax、mov eax,ecx。返回int 值在寄存器rax 的低DWORD 中,并且寄存器r13d 已清零。因此,如果 "whatIsCalled" 失败,则返回其失败代码,否则返回 LastRetCode 全局变量的值。
    • 这在 Python 中你将执行 retcode = subprocess.Popen('call bug.cmd', shell=True).wait()。
    【解决方案4】:

    使用 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%
    

    退出代码为 1

    【讨论】:

    • 伟大的思想都一样。除了没有call.exe。它内置于cmd.exe。在任何调用CreateProcess的环境中,OP 可能都需要cmd /c call bug.bat。
    • call 适用于完成后将运行时返回到当前线程。我相信这就是导致退出状态冒泡的原因,而没有call 的隐含cmd /c(来自注册表中的.bat 和.cmd 文件的关联)退出0,只是因为它能够成功启动bug.bat,无论其子线程的退出状态如何。无论如何,这是我的猜测。我确信 Aacini、MC ND、dbenham、npocmaka 或任何其他常驻神灵都可以提供更准确的见解。
    • 感谢您找到解决方法,但是我认为答案没有抓住重点:我不仅对使事情正常进行感兴趣,而且还想了解是什么解释了两者的行为差异非常相似脚本。
    【解决方案5】:

    @dbenham 的回答很好。我并不是要提出其他建议。但是,我发现使用一个变量作为返回码和一个共同的退出点是可靠的。是的,它需要一些额外的行,但也允许额外的清理,如有必要,必须将其添加到每个退出点。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-06-27
      • 1970-01-01
      • 2022-01-27
      • 2022-06-21
      • 1970-01-01
      • 2012-12-24
      • 2023-03-15
      • 2012-07-18
      相关资源
      最近更新 更多