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):
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.
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.
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:
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
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:
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:
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:
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
@exit /b 1 & echo NOT EXECUTED
Here are some test runs showing that the behavior is the same:
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:
@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:
@exit /b 1 || rem
But the following works fine without any problem.
@exit /b 1 && rem
And so does this work
@if 1==1 (exit /b 1) else rem