Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between "|| exit /b" and "|| exit /b !errorlevel!"

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).

like image 936
Chris Oldwood Avatar asked Feb 17 '21 18:02

Chris Oldwood


People also ask

What is Errorlevel in batch file?

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 )

What does exit B mean?

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.


2 Answers

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"

      • If needed, escape any embedded " characters as `", such as around batch-file paths and pass-through arguments that contain spaces:
        cmd /c ".\test.bat `"quoted argument`" & exit"
      • Alternatively, if you don't need PowerShell's string interpolation to embed variable values in the call, you can use '...' 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!

like image 72
mklement0 Avatar answered Oct 11 '22 09:10

mklement0


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?

like image 36
jeb Avatar answered Oct 11 '22 08:10

jeb