Unexpected exit code when running a batch file from PowerShell - powershell

I am calling a .bat file from Powershell in a Microsoft Windows environment, and would like to know if the exit code was success or failure.
A problem seems to be occurring in this unusual, (although not very unusual), edge-case.
Example #1:
.\test1.bat
ECHO ON
setlocal enabledelayedexpansion
False
if !errorlevel! neq 0 exit /b !errorlevel!
echo "More text"
This works fine. False causes an error; the next line checks the status, sees a 1, and exits with code 1. The calling powershell has the correct result. echo $LASTEXITCODE is 1.
Example #2:
.\test2.bat
ECHO ON
setlocal enabledelayedexpansion
if "testing" == "testing" (
False
if !errorlevel! neq 0 exit /b !errorlevel!
echo "More text"
echo "More code"
True
if !errorlevel! neq 0 exit /b !errorlevel!
echo "More code"
)
Here is the difficulty. False causes an error; the next line checks the status, sees a 1, and exits with code 1. Then, the calling powershell has an unexpected result, echo $LASTEXITCODE is 0.
Could only the batch file be modified to fix the problem?

You're seeing problematic cmd.exe behavior when batch files are called from outside cmd.exe, such as from PowerShell, their exit codes aren't reliably relayed as cmd.exe's process exit code.
For background information, see this answer.
Workarounds:
Call your batch file as follows:
cmd /c '.\test2.bat & exit'
Alternatively, if installing a third-party module (authored by me) is an option, use the ie function from the Native module (Install-Module Native), which additionally compensates for PowerShell's broken argument-passing to external programs (see this answer):
# Assuming `Install-Module Native` was run:
ie .\test2.bat

Related

exit /b not working properly inside block with further statements

I'm working on a problem in a codebase where a perl script calls a batch script and does stuff based on the exit code. In some cases, although statements like exit /b 1 are executed and the batch script exits, the exitcode is seen as 0 by the perl script. I've narrowed it down to the following example.
Here's a minimal version of the perl script:
#!/usr/bin/perl
print "calling bat with #ARGV\n";
qx(batscript.bat #ARGV);
my $exitcode = $? >> 8;
print "return code from bat is $exitcode \n";
And here's a minimal version of the batch script:
#echo OFF
setlocal enableextensions
if "%~1" == "err" (
echo "non-zero code requested"
exit /b 1
echo hello
)
endlocal
This is what I get:
c:\tmp>plscript.pl
calling bat with
return code from bat is 0
c:\tmp>plscript.pl err
calling bat with err
return code from bat is 0
If I remove that echo hello line from the batch script, it works properly:
c:\tmp>plscript.pl
calling bat with
return code from bat is 0
c:\tmp>plscript.pl err
calling bat with err
return code from bat is 1
Is this a bug in how batch runs and processes statements in blocks? It would be preferable not to have to refactor the batch script since it's quite big and has many exit /b statements.
Based on a suggestion from aschipfl, I eventually settled on changing the way the batch script is called, by using CALL. I like this solution because I don't have to modify the batch script (which can be called either by the perl script or by users directly from a cmd.exe window).
So basically in the perl script I changed the 3rd line from:
qx(batscript.bat #ARGV);
to:
qx(call batscript.bat #ARGV);

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).
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!
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?
Let's look at the three possible scenarios:
cmd /c "exit 99" || exit /b
returns 0 because cmd /c "exit 99" executed correctly
cmd /c "exit 99" || exit /b !errorlevel!
returns 99 because cmd /c "exit 99" set errorlevel to 99 and we are returning the errorlevel which results from executing cmd /c "exit 99"
cmd /c "exit 99" || exit /b %errorlevel%
returns ? - errorlevel as it was when the cmd /c "exit 99" line was parsed as it was
at that time that `%errorlevel% was evaluated.
If delayedexpansion was not set, then the only difference is that the !errorlevel! scenario attempts to assign a string to the error level, which most probably won't work very well.
As for Powershell - it's a corner case on a road less travelled. A scenario that was not tested thoroughly as the designers expected to execute .exes, etc. using this facility. No doubt even if it is reported, it would not be fixed as there's a workaround, even if it's not well-exposed.
This is, I believe, the fail-to-fail scenario - a facility that's assumed to work because the right conditions to cause it to fail are rarely met.
In the same way,
echo %%%%b
is ambiguous. Does it mean to echo the literal %%b or to echo % prefixed to the contents of metavariable b? (Answer : the latter). Not exactly encountered every day. What chance that the ambiguity will be resolved - ever? We can't even get /u implemented on the date command to have it deliver the date in a universal format which would solve the vast majority of date-oriented questions posted on the batch tag. As for the possibility of a switch to allow date to deliver days-since-some-epoch-date - I haven't even bothered to suggest it since, despite inviting suggestions for cmd modifications, absolutely nothing has been done about facilities offered, just user-interface changes. Off playing with all the shiny things while ol' faithful languishes in the bottom of a locked filing cabinet stuck in a disused lavatory with a sign on the door saying ‘Beware of the Leopard.”
Eliminating the irrelevant PowerShell and Cmd.exe invocation complexities so we can see how the cmd interpreter works:
#setlocal EnableExtensions EnableDelayedExpansion
#set prompt=$G
call :DefaultExit
#echo %ErrorLevel%
call :ExplicitExit
#echo %ErrorLevel%
#rem ErrorLevel is now 2
call :DefaultExit || echo One-liner ErrorLevel==%ErrorLevel%, not zero!
#echo %ErrorLevel%
#rem Reset the errorlevel to zero
call :DefaultExit
#echo %ErrorLevel%
call :ExplicitExit || echo One-liner ErrorLevel==!ErrorLevel!
#echo %ErrorLevel%
#exit /b
:DefaultExit
exit /b
:ExplicitExit
exit /b 2
Yields:
> test
>call :DefaultExit
>exit /b
0
>call :ExplicitExit
>exit /b 2
2
>call :DefaultExit || echo One-liner ErrorLevel==2, not zero!
>exit /b
One-liner ErrorLevel==2, not zero
2
>call :DefaultExit
>exit /b
2
>call :ExplicitExit || echo One-liner ErrorLevel==!ErrorLevel!
>exit /b 2
One-liner ErrorLevel==2
2
As you can see, exit /b is exactly equivalent to exit /b %ErrorLevel%, which is NOT what you want with your || failure execution path. You must invoke delayed expansion to get the results from the left side of the || operator.
I ran one more quick test of the above script and interactively checked the error level from the console window after the script exited:
> echo %errorlevel%
2
As you can see, exit /b alone, returned 2, which was the previously set error level. So my hunch seems correct. You definitely require delayed expansion of some form !ErrorLevel! or %^ErrorLevel% (as per #aschipfl).
The & operator of PowerShell internally invokes cmd.exe /C when running a batch file. I bet $LastExitCode refers to invocation of cmd.exe rather than to the batch file. In this context, call passes over the ErrorLevel from the batch file to cmd.exe.
The same issue arises in cmd.exe (with PowerShell not involved at all), running .\test.bat & call echo %^ErrorLevel% does not invoke cmd.exe /C to run the batch file, so this works (meaning it always returns 99). However, cmd /C .\test.bat & call echo %^ErrorLevel% just fails like your PowerShell attempt (returning 0 with your batch file as it stands), and cmd /C call .\test.bat & call echo %^ErrorLevel% succeeds and therefore returns 99.
By the way, call echo %^ErrorLevel% just enables us not having to use delayed expansion.

How to suppress exit status echo?

I have a batch file that calls a powershell script that calls the same batch file again which calls a different powershell script
runPowerShell.bat
scriptWrapper.ps1
Process.ps1
the flow is: runPowerShell.bat > scriptWrapper.ps1 > runPowerShell.bat > Process.ps1
the batch file contains an echo of the Process.ps1 exit code.
if %errorlevel% NEQ 0 GOTO :error
GOTO :end
:error
echo Exit Code: %ERRORLEVEL%
echo Failed!
EXIT /B %ErrorLevel%
:end
echo Exit Code: %ERRORLEVEL%
echo Success!
The thing is, since scriptWrapper runs successfully as well, it also seems to pass back a success even though i have no exit codes in the scriptWrapper.ps1 file. so essentially i end up with two success messages instead of just the one from the Process.ps1 that i want only.
Exit Code: 0
Success!
Exit Code: 0
Success!
so is there a way to suppress the echo in runPowerShell.bat to ONLY echo from the Process.ps1?
I could create another batch file to handle this scenario, but i would like to see if there is an option that doesnt have to involve another batch file here first.

How do I tell eclipse that an external tool is done executing?

The Eclipse IDE has a feature where one could run external tools. I use it to run batch scripts. Oddly if a batch script runs a powershell command, the powershell command will never exit until I hit enter. This is especially odd since it exits just fine when running in cmd. How should I correct my script so that it runs as expected via the eclipse external tools?
Current script (foo.bat):
#echo off
echo "Hello 1"
REM Configure this to your installation of maven.
SET "CMD=C:\foo.ps1"
REM Reformat args to be Powershell friendly.
SET "ARGS=%*"
SET "ARGS=%ARGS: =' '%"
PowerShell.Exe -Command "%CMD%" '%ARGS%'
echo "Hello 2"
EXIT /B
In cmd, I see "Hello 1", the output of %CMD%, and "Hello 2". In Eclipse, I see "Hello 1", the output of %CMD%, and then it hangs in the progress tab forever until I click the Console window and press the enter key.
I tried passing the -NonInteractive flag to Powershell. I tried having my Powershell script echo a newline at the end. Not sure how to get this to "just work".
Found the answer. I needed to add a NUL redirect to the end of my Powershell command. So it looks like this:
#echo off
REM Configure this to your installation of maven.
SET "CMD=C:\foo.ps1"
REM Reformat args to be Powershell friendly.
SET "ARGS=%*"
SET "ARGS=%ARGS: =' '%"
PowerShell.Exe -Command "%CMD%" '%ARGS%' < NUL
Note that I also removed the dubugging code from the script found in my question. If you add that code back in, you'll see that it echos everything now.

How to check in Windows if the service is not installed using batch file

I'm trying to do a batch program that needs to check if the service is installed before checking if it is running/stopped.
I would just like to ask if there is any way that I can check the ERRORLEVEL of an uninstalled service when the OS is Windows XP.
In my code snippet:
ver | find /I "XP"
if %errorlevel%==0 goto ver_xp
goto ver_nonXP
:ver_xp
echo Windows XP
sc query myService > nul
echo %errorlevel%
if errorlevel ___I goto ServiceOk
if errorlevel ___ goto ServiceError
goto ServiceError
:ver_nonXP
echo Windows is not XP
sc query myService > nul
echo error1_percent %errorlevel%
if %errorlevel%==0 goto ServiceOk
if %errorlevel% NEQ '0' goto ServiceError
goto end
:ServiceError
echo Service is not installed
net helpmsg %errorlevel%
goto end
:ServiceError
rem do some operations here....
I tried to use
if errorlevel 1060 goto ServiceError
It seems that if the service is not installed, the condition above will always be false.
I made the errorlevel ____ because I don't know that the correct condition should be.
sc query myService |find "myService" >nul will do the trick
According to this answer this is possible in batch using method you described How does one find out if a Windows service is installed using (preferably) only batch?
Alternatively you could query with powershell:
$serv_status = get-service "myService"
if($serv_status -ne $null)
{
// do some operations here
}