7

I suspect there is no good solution, but perhaps I'm overlooking something:

What I'm after is a way to:

  • (a) call a batch file from PowerShell in a way that robustly reflects its - implicit or explicit - exit code in PowerShell's automatic $LASTEXITCODE variable.

    • Notably, calling a batch file that exits with, say, whoami -nosuch || exit /b, should result in $LASTEXITCODE reflecting whoami's exit code, i.e. 1. This is not the case when you invoke a batch file (by name or path) from PowerShell: the exit code is 0 (by contrast, inside a cmd.exe session %ERRORLEVEL% is set to 1).

    • Also note that the invocation should remain integrated with PowerShell's output streams, so I am not looking for solutions based on System.Diagnostics.Process.

    • Furthermore, I have no knowledge of or control over the batch files getting invoked - I'm looking for a generic solution.

  • (b) without double-quoted arguments passed to the batch file getting altered in any way, and without cmd.exe's behavior getting modified in any way; notably:

    • ^ characters should not be doubled (see below).
    • Enabling delayed expansion with /V:ON is not an option.

The only way I know how to solve (a) is to invoke the batch file via cmd /c call.

Unfortunately, this violates requirement (b), because the use of call seemingly invariably doubles ^ characters in arguments. (And, conversely, not using call then doesn't report the exit code reliably).

Is there a way to satisfy both requirements?

Note that PowerShell is only the messenger here: The problem lies with cmd.exe, and anyone calling a batch file from outside a cmd.exe session is faced with the same problem.


Example (PowerShell code):

# Create a (temporary) batch file that echoes its arguments,
# provokes an error, and exits with `exit /b` *without an explicit argument*.
'@echo off & echo [%*] & whoami -nosuch 2>NUL || exit /b' | Set-Content test.cmd

# Invoke the batch file and report the exit code.
.\test.cmd "a ^ 2"; $LASTEXITCODE

The output should be:

["a ^ 2"]
1

However, in reality the exit code is not reported:

["a ^ 2"]
0          # !! BROKEN

If I call with cmd /c call .\test.cmd instead, the exit code is correct, but the ^ characters are doubled:

PS> cmd /c call .\test.cmd "a ^ 2"; $LASTEXITCODE
["a ^^ 2"]  # !! BROKEN
1           # OK
3
  • 1
    This works for me: cmd '/c .\test.cmd "a ^ 2" & exit /b' . It returns correct output from your test batch file and if I put gibberish there I get 9009 in $LASTEXITCODE (MSG_DIR_BAD_COMMAND_OR_FILE). Commented Apr 6, 2021 at 21:45
  • That's great, @beatcracker - please post it as an answer, and I'll accept it. I had tried || exit /b, which should work; trying & exit /b hadn't even occurred to me - it suggests that cmd.exe itself isn't aware that the batch file failed, yet somehow does pass its nonzero exit code through with exit /b. 🤦‍♂️ Commented Apr 6, 2021 at 21:51
  • 2
    You mentioning that the %ERRORLEVEL% is set to 1 gave me a hint that something like this might work ;). Commented Apr 6, 2021 at 22:02

2 Answers 2

6

I've no idea why this works, but it does:

cmd /c '.\test.cmd "a ^ 2" & exit'
$LASTEXITCODE

Output:

["a ^ 2"] 
1
Sign up to request clarification or add additional context in comments.

Comments

3

Kudos to beatcracker for finding an effective workaround in his answer; let me add some background information and guidance:

  • First, to be clear, no workaround should be necessary; cmd.exe's behavior is clearly a bug.

  • cmd /c '.\test.cmd "a ^ 2" || exit' - i.e. || rather than & - is what one would expect to be an effective workaround too. The fact that only &, which unconditionally sequences commands, works, indicates that even cmd.exe-internally the failure status of the batch file isn't yet known as part of the same statement - only afterwards - which appears to be another manifestation of the bug.

    • Why an explicit exit call following the batch-file call as part of the same statement does relay the batch file's (zero or nonzero) exit code correctly is anyone's guess, but it seems to work.
  • Fortunately, the workaround is also effective for solving related exit-code problems in batch files that do not contain explicit exit /b / exit calls - see this answer.

Syntax considerations:

  • From PowerShell, the alternative to passing a single command-string is to pass individual arguments and escape the & character as `& (using `, the "backtick", PowerShell's escape character) so as to prevent PowerShell from interpreting it (quoting it as '&' would work too):

    cmd /c .\test.cmd "a ^ 2" `& exit
    
  • From an environment that doesn't involve a shell, such as when launching from Task Scheduler, the `-escaping of & is not needed (and mustn't be used).

Not having to enclose the entire for-cmd.exe command in quotes makes it easier to pass arguments that (a) individually require double quotes and (b) involve references to PowerShell variables and/or expressions, given that the latter requires use of "..." rather than '...':

# Passing *individual* arguments makes double-quoting easier.
PS> cmd /c .\test.cmd "Version = $($PSVersionTable.PSVersion)" `& exit; $LASTEXITCODE
["Version = 7.2.0-preview.4"]
1

Using quoting of the entire for-cmd.exe command would be awkward in this case, due to the need to escape the argument-specific " chars.:

# Embedded double quotes must now be `-escaped.
PS> cmd /c ".\test.cmd `"Version = $($PSVersionTable.PSVersion)`" & exit"
["Version = 7.2.0-preview.4"]
1

The Native module (authored by me; install it from the PowerShell Gallery with Install-Module Native) comes with function ie, which:

  • automatically applies the above workaround.

  • generally compensates for problems arising from PowerShell's broken argument-passing to external programs (see this answer).

# After having run Install-Module Native:
# Use of function `ie` applies the workaround behind the scenes.
PS> ie .\test.cmd "Version = $($PSVersionTable.PSVersion)"; $LASTEXITCODE
["Version = 7.2.0-preview.4"]
1

The hope is that what function ie does will become a part of PowerShell itself, as part of the upcoming (in PowerShell v7.2) PSNativeCommandArgumentPassing experimental feature that is intended as an opt-in fix to the broken argument-passing - see GitHub issue #15143

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.