Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.1k views
in Technique[技术] by (71.8m points)

powershell - 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):

& .est.bat; $LastExitCode

Here is the output depending on which of the two lines in the batch file is executed:

PS> & .est.bat; $LastExitCode
0
PS> & .est.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 .est.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).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 .est.bat `& exit

      • Note the `-escaped &, which prevents PowerShell from interpreting & up front. Omit the ` if you're running the command from an environment that doesn't involve a shell, such as from aa scheduled task.
      • Alternatively, you can use the form cmd /c "<batch-file> ... & exit", but that requires escaping of any " characters that are part of arguments (or the use of a here-string). If no PowerShell variables or expressions are being used, single-quoting is an option, which avoids that problem: cmd /c '<batch-file> ... & 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 & .est.bat (or, in this simple case, just .est.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!


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...