3

Please, observe:

c:\temp\1.cmd

@echo off
setlocal

cmd /c dir aaa
IF %ERRORLEVEL% NEQ 0 GOTO fail
GOTO end
:fail
echo - Script failed

:end
endlocal

Now if I run it in the command prompt:

C:\temp> cmd
Microsoft Windows [Version 10.0.16299.967]
(c) 2017 Microsoft Corporation. All rights reserved.

C:\temp>c:\temp\1.cmd
 Volume in drive C has no label.
 Volume Serial Number is 4A5E-F223

 Directory of C:\temp

File Not Found
- Script failed

C:\temp>echo %errorlevel%
1

C:\temp>

Now I am running it from Powershell:

C:\temp> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.16299.967
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.16299.967
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1


C:\temp> cmd /c C:\temp\1.cmd
 Volume in drive C has no label.
 Volume Serial Number is 4A5E-F223

 Directory of C:\temp

File Not Found
- Script failed
C:\temp> $LASTEXITCODE
0
C:\temp>

From what I know the exit code is supposed to propagate correctly. So, what is the problem?

mark
  • 59,016
  • 79
  • 296
  • 580

3 Answers3

7

Note: This answer was substantially rewritten after new information came to light.

To complement jazzdelightsme's effective solution with some general guidance and background information:

  • When calling a batch file from outside cmd.exe, such as from PowerShell, invoke it as cmd /c 'file.cmd ... & exit' or - if string interpolation is needed - as
    cmd /c "file.cmd ... & exit", which then requires escaping embedded " as `"[1]. This ensures that it behaves as it would when called from inside cmd.exe with respect to its exit code (error level), i.e. ensures that the batch file's exit code reliably becomes cmd.exe's process exit code.

    • This post explains the problem and this - obscure - workaround in detail.
    • Note: The simpler workaround based on call - cmd /c call file.cmd ... - works in principle, but has an unwanted side effect when (of necessity double-quoted[2]) arguments with ^ characters are passed: Due to use of call, ^ characters ("carets", strictly speaking circumflex accents, U+005E) are invariably doubled, so that say, argument "a ^ 2", is seen by the batch file as "a ^^ 2"; it is unclear what the purpose of this behavior is, but it has seemingly always existed - see this answer for details on cmd.exe's parser.
  • Without the & exit workaround, you'll only get the desired behavior if you ensure that all code paths exit in one of the following ways - and missing a code path is an easy mistake to make:

    • exit with no arguments, which correctly relays the most recently executed command's exit code.

      • Caveat: exit without /b instantly exits the cmd.exe instance as a whole, so it isn't suitable for use inside batch files that may be run interactively or must be callable from other batch files and return control to those batch files.
    • exit /b <code> or exit <code>, where <code> represents the desired exit code, i.e. specifying an explicit exit code, as in jazzdelightsme's solution.

      • Caveat: Exiting a batch file with exit /b without an explicit <code> argument does not pass the most recently executed command's exit code through without the cmd /c <batch-file> ... `& exit workaround - see this answer.

Additional background information, in the context of your code:

Bizarrely, with an outside invocation of a batch file without & exit (or call), statements such as if, goto, echo, and endlocal, and even REM (but, curiously, not ::) reset the exit code that cmd.exe later reports to 0 - even though inside that cmd.exe session %ERRORLEVEL% is set as it usually is, meaning that such statements have no impact on the current %ERRORLEVEL% value.

Therefore:

  • When your batch file is run from inside cmd.exe, the specific statements that follow the command that sets %ERRORLEVEL% to 1 (cmd /c dir aaa), i.e. the if, goto, echo and endlocal statements, preserve this value, and %ERRORLEVEL% ends up with value 1 after exiting the batch file.

    • Bizarrely, the error level is set only after the statement involving the batch file call, so that something like file.cmd || exit does not work, because the || operator doesn't recognize the file.cmd call as having failed. This is the reason that the workaround uses & rather than ||.
  • When your batch file is run from outside cmd.exe, and isn't invoked with cmd /c "<batch-file> ... & exit" (or cmd /c call <batch-file> ...), the same statements that follow the %ERRORLEVEL%-setting command suddenly reset the exit code that cmd.exe itself later reports to 0, instead of preserving the post-batch-file-execution %ERRORLEVEL% value.


[1] Situationally, you can get away with cmd /c <batch-file> ... `& exit, i.e. without enclosing the arguments to /c in a single, overall string, but that breaks if the batch-file path needs double-quoting and at least one double-quoted pass-through argument is also present.

[2] ^ chars. in unquoted arguments, as usual, are interpreted as cmd.exe's escape character and therefore removed.

mklement0
  • 382,024
  • 64
  • 607
  • 775
1

First: ERRORLEVEL is not %ERRORLEVEL%.

Second, the errorlevel is not the same as a process exit code.

Try altering your cmd script as follows (note the addition of "exit /b 1"):

@echo off
setlocal

cmd /c dir aaa
IF %ERRORLEVEL% NEQ 0 GOTO fail
GOTO end
:fail
echo - Script failed
exit /b 1

:end
endlocal
jazzdelightsme
  • 457
  • 3
  • 14
  • I will accept it, but it seems a bit strange, because `dir` does set the environment variable `%ERRORLEVEL%`. May be it is in addition to the ERRORLEVEL, but nonetheless, the environment variable is set. So, it is in effect the exit code. – mark Mar 21 '19 at 21:51
0

I don't exactly know why but I had the same problem. $LastExitCode was always 0 even if the batch file exited with an errorCode of 1.

I needed the $LastExitCode so that in case of error, I had to prompt an error an exit the ps1 script, so what I did was wrapping the batch execution command inside an if like this:

if( !(Start-Process -FilePath PATH/TO/BATCH -NoNewWindow -Wait -ErrorAction -Stop -ArgumentList arguments_if_needed) ){
   
    Write-Error "There was an error, exiting"
    exit
}

This is working for me.

andrestascon
  • 64
  • 1
  • 8