Search code examples
pythonwindowsbatch-fileexit-code

How to properly report an exit status in batch?


I'm facing a weird situation where a batch file I wrote reports an incorrect exit status. Here is a minimal sample that reproduces the problem:

bug.cmd

echo before

if "" == "" (
        echo first if
        exit /b 1

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

echo after

If I run this script (using Python but the problem actually occurs when launched in other ways too), here is what I get:

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

Note how exit status is reported as 0 even though exit /b 1 should make it be 1.

Now the weird thing is that if I remove the inside if clause (which should not matter because everything after exit /b 1 should not be executed anyway) and try to launch it:

ok.cmd

echo before

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

echo after

I launch it again:

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

Now the exit status is correctly reported as 1.

I'm at loss understanding what is causing this. Is it illegal to nest if statements ?

How can I signal correctly and reliably my script exit status in batch ?

Note: calling exit 1 (without the /b) is not an option as it kills the whole interpreter and prevents local script usage.


Solution

  • As @dbenham notes, "[i]f a command is parsed after EXIT /B, within the same command block, then the problem manifests, even though the subsequent command never executes". In this particular case the body of the IF statement is basically evaluated as

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

    where the & operator is the function cmd!eComSep (i.e. command separator). The EXIT /B 1 command (function cmd!eExit) is evaluated by setting the global variable cmd!LastRetCode to 1 and then basically executing GOTO :EOF. When it returns, the second eComSep sees cmd!GotoFlag is set and so skips evaluating the right-hand side. In this case, it also ignores the return code of the left-hand side to instead return SUCCESS (0). This gets passed up the stack to become process exit code.

    Below I've included the debug sessions for running bug.cmd and 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
    

    In the ok.cmd case, cmd!eComSep only appears once in the stack trace. The exit /b 1 command is evaluated as the right-hand side operand, so the code that looks at GotoFlag never runs. Instead the return code of 1 gets passed up the stack to become the process exit code.