We have a bunch of .bat
build scripts which are invoked by a PowerShell based GitLab runner that were recently refactored from:
program args
if !errorlevel! neq 0 exit /b !errorlevel!
to the more succinct:
program args || exit /b
Today I investigated a build job which obviously failed if you looked at the error log but which was reported as a success. After much experimentation I discovered that this pattern didn't always work as expected:
program args || exit /b
but this did appear to work when the former didn't:
program args || exit /b !errorlevel!
I've read the SO question Windows batch exit option b with or without errorlevel and the statement below from https://www.robvanderwoude.com/exit.php but still can't quite explain what I'm observing.
The DOS online help (HELP EXIT) doesn't make it clear that the /B parameter exits the current instance of script which is not necessarily the same as exiting the current script. I.e. if the script is in a CALLed piece of code, the EXIT /B exits the CALL, not the script.
This is the minimal batch file I used to explore this:
@echo off
setlocal EnableDelayedExpansion
cmd /c "exit 99" || exit /b
:: cmd /c "exit 99" || exit /b !errorlevel!
And this is how I invoked the batch file (to simulate how it was invoked by the GitLab PowerShell based runner):
& .\test.bat; $LastExitCode
Here is the output depending on which of the two lines in the batch file is executed:
PS> & .\test.bat; $LastExitCode
0
PS> & .\test.bat; $LastExitCode
99
There is another way to get the correct behaviour which is to invoke the batch file longhand from within PowerShell using CALL
as well:
PS> & cmd.exe /c "call .\test.bat"; $LastExitCode
99
While I appreciate that this may be the correct way to invoke a batch file from PowerShell, that does not appear to be common knowledge based on the many examples I've seen. Also I wonder why PowerShell doesn't invoke a batch file this way if it's "the right way". Lastly I still don't understand why, when leaving off the call
, the behaviour changes depending on whether we add the !errorlevel!
to the exit /b
statement.
UPDATE: Thanks for all the discussion so far but I feel it's getting lost in the weeds which is probably my fault for being too vague in my original question. What I think I'm really after (if possible) is a definitive statement about when the errorlevel
is (supposedly) evaluated in the following statement:
program || exit /b
Is it really evaluated early (i.e. before program
is run) like this:
program || exit /b %errorlevel%
Or is it evaluated lazily (i.e. when exit
is being executed after program
has run and internally errorlevel
has been updated), more analogous to this:
program || exit /b !errorlevel!
Hence I'm not really after speculation unless, sadly, that is the best that we can do in which case knowing there is no definitive answer or that it's a bug is an acceptable answer to me :o).
Batch file error level: %ERRORLEVEL% is an environment variable that contains the last error level or return code in the batch file – that is, the last error code of the last command executed. Error levels may be checked by using the %ERRORLEVEL% variable as follows: IF %ERRORLEVEL% NEQ 0 ( DO_Something )
EXIT /B at the end of the batch file will stop execution of a batch file. use EXIT /B < exitcodes > at the end of the batch file to return custom return codes. Environment variable %ERRORLEVEL% contains the latest errorlevel in the batch file, which is the latest error codes from the last command executed.
Workarounds:
Call your batch file via cmd /c "<batch-file> ... & exit"
, in which case the || exit /b
solution without an explicit exit code works as expected:
cmd /c ".\test.bat & exit"
"
characters as `"
, such as around batch-file paths and pass-through arguments that contain spaces: cmd /c ".\test.bat `"quoted argument`" & exit"
'...'
quoting, in which case embedded "
can be used as-is: cmd /c '.\test.bat "quoted argument" & exit'
Using cmd /c "<batch-file> ... & exit"
routinely to call batch files from outside cmd.exe
is advisable, as even batch files without explicit exit /b
(or exit
) calls can otherwise behave unexpectedly - see this answer.
Alternatively - but only if your batch file never needs to be called from another batch file to which control should be returned and if it never needs to be part of a cmd /c
multi-command command line where it isn't the last command[1] - you can use || exit
instead of || exit /b
- this exits the executing cmd.exe
process as a whole, instantly, but the exit code (error level) is then reliably reported (at least in the context of a <command> || exit
statement) also with direct invocation from outside cmd.exe
, such as & .\test.bat
(or, in this simple case, just .\test.bat
) from PowerShell.
While combining setlocal EnableDelayedExpansion
with exit /b !ERRORLEVEL!
works too (except inside (...)
- see this post) - due to using an explicit exit code - it is obviously more cumbersome and can have side effects, notably quietly removing !
characters from commands such as echo hi!
(while it's possible to minimize that problem by placing the setlocal EnableDelayedExpansion
call on the line just before an exit /b
call, that would require duplication if there are multiple exit points).
cmd.exe
's behavior is unfortunate in this case, but can't be avoided.
When calling a batch file from outside cmd.exe
:
exit /b
- without an exit-code (error-level) argument - only sets the cmd.exe
process exit code as expected - namely to the exit code of the most recently executed command in the batch file - if you follow the batch-file call with & exit
, i.e. as cmd /c <batch-file> ... `& exit
Without the & exit
workaround, an argument-less exit /b
call from a batch file is reflected in the %ERRORLEVEL%
variable intra-cmd.exe
-session, but that doesn't translate to cmd.exe
's process exit code, which then defaults to 0
.[1]
With the & exit
workaround, intra-batch-file argument-less exit /b
does properly set cmd.exe
's exit code, even in a <command> || exit /b
statement, in which case <command>
's exit code is relayed, as intended.
exit /b <code>
, i.e. passing an exit code <code>
explicitly, always works[2], i.e. the & exit
workaround is then not needed.
This distinction is an obscure inconsistency that could justifiably be called a bug; Jeb's helpful answer has sample code that demonstrates the behavior (using the less comprehensive cmd /c call ...
workaround as of this writing, but it applies equally to cmd /c "... & exit"
).
[1] With cmd /c
, you can pass multiple statements for execution, and it is the last statement that determines the cmd.exe
process' exit code. E.g, cmd /c "ver & dir nosuch"
reports exit code 1
, because the non-existent file-system item nosuch
caused dir
to set the error level to 1
, irrespective of whether or not the preceding command (ver
) succeeded. The inconsistency is that, for a batch file named test.bat
which exits with exit /b
without an explicit exit-code argument, cmd /c test.bat
always reports 0
, whereas cmd /c test.bat `& exit
properly reports the exit code of the last statement executed before the batch file exited.
[2] The exit code may be specified literally or via a variable, but the pitfall is that - due to cmd.exe
's up-front variable expansion - <command> || exit /b %ERRORLEVEL%
does not work as intended, because %ERRORLEVEL%
at that point expands to the error level prior to this statement, not to the one set by <command>
; this is why delayed expansion, via having run setlocal enabledelayedexpansion
or having invoked the cmd.exe
with the /V
option, is necessary in this case: <command> || exit /b !ERRORLEVEL!
There is a difference between exit /b
and exit /b <code>
.
As mklement0 states, the difference becomes visible when calling a batch file with or without CALL
In my tests, I used (call)
to force the errorlevel to 1.
test1.bat
@echo off
(call)
exit /b
test2.bat
@echo off
(call)
exit /b %errorlevel%
Testing with test-all.bat:
cmd /c "test1.bat" & call echo Test1 %%errorlevel%%
cmd /c "call test1.bat" & call echo call Test1 %%errorlevel%%
cmd /c "test2.bat" & call echo Test2 %%errorlevel%%
cmd /c "call test2.bat" & call echo call Test2 %%errorlevel%%
Output:
Test1 0 call Test1 1 Test2 1 call Test2 1
To get an always reliable errorlevel, you should use the explicit form of exit /b <code>
.
In case of using the construct <command> || exit /b !errorlevel!
the delayed expansion is necessary or the form
<command> || call exit /b %%errorlevel%%
Another solution
<command> || call :exit
...
:exit
(
(goto) 2>nul
exit /b
)
This uses the batch exception handling
Does Windows batch support exception handling?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With