Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 928 Vote(s) - 3.48 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How to properly report an exit status in batch?

#1
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.
Reply

#2
The following is working ok invoking the bat with **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%

> Exit Code is 1
Reply

#3
@dbenham's answers are good. I am not trying to suggest otherwise. But, I have found it reliable to use a variable for the return code and a common exit point. Yes, it takes a few extra lines, but also allows additional cleanup that, if necessary, would have to be added to every exit point.

@ECHO OFF
SET EXITCODE=0

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

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

:TheEnd
EXIT /B %EXITCODE%
Reply

#4
Wow! that is freaky!

I am able to reproduce the apparent bug from the command line console by running the following (note I use `/Q` to turn ECHO OFF so output is simpler):

<!-- language: lang-none -->

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

D:\test>echo %errorlevel%
0

I get the same behavior if I rename the script to "bug.bat"

I also get the expected return code of 1 if I remove the 2nd IF.

I agree, this seems to be a bug. Logically, I see no reason for the two similar scripts to yield different results.

I don't have a full explanation, but I believe I understand an important component to the behavior: The batch ERRORLEVEL and the exit code do not refer to the same thing! Below is the documentation for the EXIT command. The important bit is the description of the exitCode parameter.

<!-- language: lang-none -->

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.

I think the average person (including myself) does not typically distinguish between the two. But CMD.EXE seems to be very finicky as to when the batch ERRORLEVEL is returned as the exit code.

It is easy to show that the batch script is returning the correct ERRORLEVEL, yet the ERRORLEVEL is not being returned as the CMD exit code. I display the ERRORLEVEL twice to demonstrate that the act of displaying it is not clearing the ERRORLEVEL.

<!-- language: lang-none -->

D:\test>cmd /q /v:on /c "bug.cmd&echo !errorlevel!&echo !errorlevel!"
before
first if
1
1

D:\test>echo %errorlevel%
0

As others have pointed out, using CALL does cause the ERRORLEVEL to be returned as the exit code:

<!-- language: lang-none -->

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

D:\test>echo %errorlevel%
1

But that doesn't work if another command is executed after the CALL

<!-- language: lang-none -->

D:\test>cmd /q /v:on /c "call bug.cmd&echo !errorlevel!"
before
first if
1

D:\test>echo %errorlevel%
0

Note that the above behavior is strictly a function of CMD.EXE, having nothing to do with the script, as evidenced by:

<!-- language: lang-none -->

D:\test>cmd /q /v:on /c "cmd /c exit 1&echo !errorlevel!"
1

D:\test>echo %errorlevel%
0

You could explicitly EXIT with the ERRORLEVEL at the end of the command chain:

<!-- language: lang-none -->

D:\test>cmd /q /v:on /c "call bug.cmd&echo !errorlevel!&exit !errorlevel!"
before
first if
1

D:\test>echo %errorlevel%
1

Here is the same thing without delayed expansion:

<!-- language: lang-none -->

D:\test>cmd /q /c "call bug.cmd&call echo %errorlevel%&exit %errorlevel%"
before
first if
1

D:\test>echo %errorlevel%
1

Perhaps the simplest/safest work around is to change your batch script to `EXIT 1` instead of `EXIT /B 1`. But that may not be practical, or desirable, depending on how others may use the script.

***EDIT***

I've reconsidered, and now think it is most likely an unfortunate design "feature" rather than a bug. The IF statements are a bit of a red herring. If a command is parsed after EXIT /B, within the same command block, then the problem manifests, even though the subsequent command never executes.

**test.bat**

<!-- language: lang-none -->

@exit /b 1 & echo NOT EXECUTED

Here are some test runs showing that the behavior is the same:

<!-- language: lang-none -->

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

It doesn't matter what the 2nd command is. The following script shows the same behavior:

<!-- language: lang-none -->

@exit /b 1 & rem

The rule is that if the subsequent command would execute if the EXIT /B were something that didn't exit, then the problem manifests itself.

For example, this has the problem:

<!-- language: lang-none -->

@exit /b 1 || rem

But the following works fine without any problem.

<!-- language: lang-none -->

@exit /b 1 && rem

And so does this work

<!-- language: lang-none -->

@if 1==1 (exit /b 1) else rem
Reply

#5
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

<!-- language-all: lang-none -->

(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 ® Windows Debugger Version 6.12.0002.633 AMD64
Copyright © 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.
Reply

#6
I'm going to try to join the answers from dbenham (that checked the cases from batch code) and eryksum (that directly went to the code). Maybe doing it I could understand it.

Let's start with a `bug.cmd`

exit /b 1 & rem

From the eryksum answer and tests we know this code will set `errorlevel` variable to 1, but the general result of the command is not a *failure* as the inner functions inside `cmd` will process the concatenation operator as a function call that will return (meaning a C function returning a value) the result of the right command. This can be tested as

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

Yes, `errorlevel` is 1 but conditional execution will run the code after the `&&` as the previous command (`eComSep`) returned `SUCESS`.

Now, executed in a separate `cmd` instance

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

Here the same process that makes the conditional execution "fail" in the previous case propagates the `errorlevel 0` out of the new `cmd` instance.

But, why does the `call` case work?

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

It works because the `cmd` is coded something like (rough assembler to C)

function CallWork(){
....
ret = BatProc( whatIsCalled )
return ret ? ret : LastRetCode
}

function eCall(){
....
return LastRetCode = CallWork( ... )
}

That is, the `call` command is handled in function `eCall` that calls `CallWork` to delegate the context generation and execution to `BatProc`. `BatProc` returns the resulting value from execution of the code. We know from previous the tests that this value is 0 (but `errorlevel / LastRetCode` is 1). This value is tested inside `CallWork` (the ternary `?` operator): if the `BatProc` return value is not 0, return the value else return `LastRetCode`, that in this case is 1. Then this value is used inside `eCall` as return value AND stored inside `LastRetCode` (the `=` in the return command is an asignation) so it is returned in `errorlevel`.

If i didn't miss something, the rest of the cases are just variations over the same behaviour.
Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through