How do I get the ErrorLevel from a process started via PowerShell Start-Process called from CMD? - powershell

I'd like to be able to get the ErrorLevel of Process.exe from this example command:
C:\>PowerShell -NoP -C "Start-Process "C:\Process.exe""
If Process.exe exits with an ErrorLevel of 5, I'd like to be able to capture that via CMD, ideally in the %ErrorLevel% variable. I'm not proficient enough in PowerShell to do this.

Use the following, assuming that C:\process.exe is a console application:[1]
PowerShell -NoP -C "C:\Process.exe; exit $LASTEXITCODE"
Note: If the executable path contains spaces, enclose it in '...'; if it contains ' itself, either escape the enclosed ' chars. as '', or enclose the path in \"...\" (sic) instead - see this answer for more information.
In order to get a process' exit code, you must wait for it to terminate.
Start-Process does not wait for termination by default, unless you add the -Wait switch - but then you'd also need -PassThru in order to return a process-information object whose .ExitCode property you'd need to report via exit.
Direct execution of the target executable, as shown above, (a) results in synchronous execution of console applications[1] to be begin with and (b) automatically reflects the process exit code in the automatic $LASTEXITCODE variable variable.
Without the exit $LASTEXITCODE statement, the process' exit code would be mapped to an abstracted exit code: 0 (success) would be reported as-is, but any nonzero exit code would be mapped to 1 - see this post for more information.
Either way, PowerShell's (powershell.exe's) exit code will be reflected in cmd.exe's dynamic %ErrorLevel% variable.
[1] If your application happens to be a GUI-subsystem application, you'll indeed need a Start-Process-based solution; as you've noted in a comment yourself, the solution would be:
PowerShell -NoP -C "exit (Start-Process 'C:\Process.exe' -Wait -PassThru).ExitCode"

Related

How do I check the errorcode of a `&` Powershell command?

In a Powershell script, I run a command like:
& "C:\...\myprogram.exe"
How do I check to see if myprogram.exe returned an errorcode?
If you run it with the call operator (or without), check $LASTEXITCODE. It will always be the exit code of the last program you ran in the current session.
If you need to use Start-Process instead (such as needing to use -Wait to wait on a GUI application to complete), make sure to use -PassThru to store the process object as a variable, then check its ExitCode property. $LASTEXITCODE is not set in this case.

How to prevent exit of host and return exit code?

Ansgar Wiechers' answer works well whenever starting a new PowerShell process. https://stackoverflow.com/a/50202663/447901 This works in both cmd.exe and powershell.exe.
C:>type .\exit1.ps1
function ExitWithCode($exitcode) {
$host.SetShouldExit($exitcode)
exit $exitcode
}
ExitWithCode 23
In a cmd.exe interactive shell.
C:>powershell -NoProfile -Command .\exit1.ps1
C:>echo %ERRORLEVEL%
23
C:>powershell -NoProfile -File .\exit1.ps1
C:>echo %ERRORLEVEL%
23
In a PowerShell interactive shell.
PS C:>powershell -NoProfile -Command .\exit1.ps1
PS C:>$LASTEXITCODE
23
PS C:>powershell -NoProfile -File .\exit1.ps1
PS C:>$LASTEXITCODE
23
HOWEVER... Running the .ps1 script inside an existing interactive PowerShell host will exit the host completely.
PS C:>.\exit1.ps1
<<<poof! gone! outahere!>>>
How can I prevent it from exiting the host shell?
Do not use $host.SetShouldExit(): it is not meant to be called by user code.
Instead, it is used internally by PowerShell in response to an exit statement in user code.
Simply use exit 23 directly in your exit1.ps1 script, which will do what you want:
When run inside a PowerShell session, the script will set exit code 23 without exiting the PowerShell process as a whole; use $LASTEXITCODE to query it afterwards.
.\exit.ps1; $LASTEXITCODE # -> 23
When run via the PowerShell CLI:
with -File, the exit code set by the script automatically becomes the PowerShell process' exit code, which the caller can examine; when called from cmd.exe, %ERRORLEVEL% reflects that exit code.
powershell -File .\exit.ps1
:: This outputs 23
echo %ERRORLEVEL%
with -Command, additional work is needed, because PowerShell then simply maps any nonzero exit code to 1, which causes the specific exit code to be lost; to compensate for that, simply execute exit $LASTEXITCODE as the last statement:
powershell -Command '.\exit.ps1; exit $LASTEXITCODE'
:: This outputs 23
echo %ERRORLEVEL%
For more information about how PowerShell sets exit codes, see this answer.
If:
you do not control how your script is invoked via the CLI, yet must ensure that the correct exit code is reported even when the script is invoked via -Command,
and you're willing to assume the risk of using $host.SetShouldExit(), even though it isn't designed for direct use,
you can try the following:
function ExitWithCode($exitcode) {
if ([Environment]::CommandLine -match ( # Called via the CLI? (-File or -Command)
' .*?\b' +
[regex]::Escape([IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) +
'(?:\.ps1\b| |$)')
) {
# CAVEAT: While this sets the exit code as desired even with -Command,
# the process terminates instantly.
$host.SetShouldExit($exitcode)
}
else {
# Exit normally, which in interactive session exits the script only.
exit $exitcode
}
}
ExitWithCode 23
The function looks for the file name of the executing script on the process command line to detect whether the enclosing script is being invoked directly via the CLI, via the automatic $PSCommandPath variable, which contains the script's full path.
If so, the $host.SetShouldExit() call is applied to ensure that the exit code is set as intended even in the case of invocation via -Command.
Note that this amounts to a repurposing of the effectively internal .SetShouldExit() method.
Surprisingly, this repurposing works even if additional commands come after the script call inside the -Command string, but note that this invariably means that the success status of the truly last command - if it isn't the script call - is then effectively ignored.
This approach isn't foolproof[1],but probably works well enough in practice.
[1]
There could be false positives, given that only the file name is looked for, without extension (because -Command allows omitting the .ps1 extension of scripts being called).
There could be false negatives, if the script is being called via another script or via an alias.
How can I prevent it from exiting the host shell?
You can check if the currently running PowerShell process is a child of another PowerShell parent process, and only call $host.SetShouldExit() when that condition is true. For example:
function ExitWithCode($exitcode) {
# Only exit this host process if it's a child of another PowerShell parent process...
$parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId
$parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name
if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) }
exit $exitcode
}
ExitWithCode 23
Hope this helps.

Exit code from a batch file is not propagated to the parent powershell script

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

Returning an exit code from a PowerShell script

I have questions about returning an exit code value from PowerShell when run from a cmd.exe invocation. I found https://weblogs.asp.net/soever/returning-an-exit-code-from-a-powershell-script which has been helpful. But, the solution for PowerShell code is to add a function.
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode) exit }
Generates:
ExitWithCode : The term 'ExitWithCode' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path
was included, verify that the path is correct and try again.
At C:\src\t\e\exit5.ps1:6 char:1
+ ExitWithCode -exitcode 12345
+ ~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (ExitWithCode:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
But, placing the "exit" on a new line works. Is this just a language anomaly?
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode)
exit }
Also, this page is from 2010. Is this still the current state? Is there a better/easier way now?
As Guenther Schmitz already explained, $host.SetShouldExit($exitcode) and exit are 2 distinct statements that must be separated either with a newline or a semicolon.
Without that separation your code should have thrown a different error, though:
Unexpected token 'exit' in expression or statement.
The error you posted looks more like you tried to use that function without defining it first.
The purpose of the function is to set a proper exit code when exiting from a script regardless of how the script was run. Normally you'd run PowerShell scripts like this:
powershell.exe -File "C:\your.ps1"
And in that case a simple exit $exitcode would be sufficient:
C:\> type test1.ps1
exit 23
C:\> powershell -File .\test1.ps1
C:\> echo %errorlevel%
23
However, another way to execute PowerShell scripts is the -Command parameter (since PowerShell scripts can be run directly from PowerShell). The difference between the -File and -Command parameters is that the latter returns only 1 or 0 (indicating whether or not the script exited with a non-zero exit code), but not the exit code itself.
C:\> powershell -Command .\test1.ps1
C:\> echo %errorlevel%
1
When omitting the parameter entirely PowerShell defaults to -Command (allowing you to easily run PowerShell statements from the commandline):
C:\> powershell .\test1.ps1
C:\> echo %errorlevel%
1
Defining an exit code via $host.SetShouldExit() ensures that the exit code is returned correctly when the script is invoked via powershell. -Command. You still should exit with the actual exit code, though, because otherwise the exit code would only be set when running the script via powershell.exe -Command, but not when running the script via powershell.exe -File:
C:\> type test2.ps1
function ExitWithCode($exitcode) {
$host.SetShouldExit($exitcode)
exit
}
ExitWithCode 23
C:\> powershell -File .\test2.ps1
C:\> echo %errorlevel%
0 # ← exit without argument defaults to 0!
C:\> powershell -Command .\test2.ps1
C:\> echo %errorlevel%
23
C:\> type test3.ps1
function ExitWithCode($exitcode) {
$host.SetShouldExit($exitcode)
exit $exitcode
}
ExitWithCode 23
C:\> powershell -File .\test3.ps1
C:\> echo %errorlevel%
23
C:\> powershell -Command .\test3.ps1
C:\> echo %errorlevel%
23
Guenther Schmitz's answer solves your immediate syntax problem, but it's important to note
that $host.SetShouldExit() is not meant to be called by user code, as implied by Bruce Payette's answer.
Instead, it is used internally by PowerShell itself in response to an exit statement in user code.
The only conceivable reason to use it is to repurpose it for implementing a workaround around a limitation of exit-code reporting when a script is being called via the -Command
(-c) parameter of PowerShell's CLI:
With -Command, a script's specific non-zero exit code is always translated to 1, so the specific exit code is lost - see this answer for an overview of exit-code handling in PowerShell.
$host.SetShouldExit(), despite not being intended for this purpose, happens to overcome this limitation and ensures that the script's exit code is also reported as the PowerShell process' exit code.
This workaround must not be applied when the script is being called from an interactive PowerShell session, because it will cause the session as a whole to exit instantly, which is the subject of your follow-up question.
Reliable detection of when a script is being called via -Command is nontrivial and hard to make fully reliable, however, as shown in this answer to your follow-up question.
The better approach is not to use the $host.SetShouldExit() workaround and do the following instead.
Invoke your script via the -File CLI parameter, in which case no workaround is needed:
Just use exit $n in your script, and $n will be reported as the PowerShell process' exit code (assuming $n is an integer).
When calling via -Command, follow the script call with ; exit $LASTEXITCODE in the command string so as to ensure that the script's exit code is passed through.
Of course, you may not always be in control of how your script is invoked via the CLI; in that event, the workaround is worth considering.
$host.SetShouldExit($exitcode) and exit are two commands which have to separated. either with a return (like you mentioned) or with a semicolon:
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode); exit }
The way to return an exit code from PowerShell is to do exit $exitcode. Internally, When the runtime processes the exit keyword it will do the call to $host.SetShouldExit($exitcode) for you. But be aware that exit also exits scripts so it matters how you run your script. If want to run a script that calls exit from powershell.exe use the -File parameter rather than the -Script as in
powershell -File scriptThatCallsExit

Powershell waits on cmd.exe differently depending on environment

Consider the powershell command:
cmd.exe "/c start notepad.exe"
Using powershell.exe (console) this command completes immediately after starting the cmd/notepad process and does not wait for notepad/cmd to exit. If I want it to wait for completion I have to pipe the output to Out-Null.
Using the powershell ISE this command blocks execution until notepad/cmd is closed.
Further, if I use create a new process (of powershell.exe) in a script running in powershell.exe using System.Diagnostics.Process and redirect standard output the script now blocks execution until notepad/cmd is closed. If I don't redirect output it does not block execution.
But if I use c# to create a new process with the same settings/start info, and run the same script with redirected output it doesn't block execution.
I'm at a loss here. Obviously it has something to do with the setup of the execution and output and maybe "cmd.exe". I'm hoping someone understands what's fundamentally happening behind the scenes differently in these cases. I know the ISE and cmd.exe isn't fully supported but the I don't know why the other 3 aren't consistent.
Why do the scripts not run with the same behavior (especially the powershell console ones) and how do I get them to?
Edit:
After some troubleshooting I was able to get all the powershell.exe versions to run the same (the ISE isn't of importance to me). The odd ball where cmd.exe "/c start notepad.exe" blocks execution is when a new process is created in powershell to run powershell.exe using System.Diagnostics.Process. If output is redirected (the reason I was using System.Diagnostics.Process in the first place, Start-Process doesn't support redirecting except to a file) then calling WaitForExit() on the process object causes the blocking to occur.
Simply substituting WaitForExit() with Wait-Process (and the process ID) causes the powershell script running in the new powershell.exe to execute as expected and exit after running the command. Hopefully this information combined with #mklement0 answer will be sufficient for anyone else having similar problems.
To get predictable behavior in both the regular console and in the ISE:
Start a GUI application asynchronously (return to the prompt right away / continue executing the script):
notepad.exe
Invoking Notepad directly makes it run asynchronously, because it is a GUI-subsystem application.
If you still want to track the process and later check whether it is still running and what its exit code was on termination, use -PassThru, which makes Start-Process return a [System.Diagnostic.Process] instance:
$process = Start-Process -PassThru notepad.exe
$process.HasExited later tells you whether the process is still running.
Once it has exited, $process.ExitCode tells you the exit code (which may not tell you much in the case of a GUI application).
To wait synchronously (at some point):
Use Wait-Process $process.ID to wait (indefinitely) for the process to terminate.
Add a -Timeout value in seconds to limit the waiting period; a non-terminating error is reported if the process doesn't terminate within the timeout period, causing $? to reflect $False.
Start a GUI application synchronously (block until the application terminates):
Start-Process -Wait notepad.exe
-Wait tells Start-Process to wait until the process created terminates; this is the equivalent of cmd /c 'start /wait notepad.exe'.
There's a non-obvious shortcut to Start-Process -Wait: you can repurpose the Out-Null cmdlet to take advantage of the fact that piping invocation of a program to it makes Out-Null to wait for the program's termination (a GUI application has no stdout or stderr output, so there's nothing for Out-Null to discard; the only effect is synchronous invocation):
notepad.exe | Out-Null
In fact, this approach has two advantages:
If arguments must be passed to the GUI application, they can be passed directly, as usual - rather than indirectly, via Start-Process's -ArgumentList parameter.
In the (rare) event that a GUI application reports a meaningful process exit code (e.g, msiexec.exe), the Out-Null approach (unlike Start-Process -Wait) causes it to be reflected in the automatic $LASTEXITCODE variable.
Note: In rare cases, a GUI application may explicitly attach to the caller's console and write information to it; in order to surface that, pipe to | Write-Output instead (you'll still be able to evaluate $LASTEXITCODE) - see this answer.
Note that for console-subsystem applications (e.g., findstr.exe), synchronous execution is the default; Start-Process is only needed for GUI applications (and for special situations, such as wanting to run an application in a new console window or with elevation (run as admin)).
To run a console application or shell command asynchronously (without opening a new console window), you have the following options:
[Preferred] Use Start-Job kick off the command, and Receive-Job to receive its output / success status later.
$j = Start-Job { sleep 2; 'hi' }
To synchronously wait for this job to finish (and remove it automatically), use
Receive-Job -Wait -AutoRemoveJob $j
In PowerShell (Core) 6+:
You can use the simpler ... & syntax (as in
POSIX-like Unix shells such as bash) in lieu of Start-Job; e.g.:
$j = & { sleep 2; 'hi!' } &
Better yet, you can use the lighter-weight, faster Start-ThreadJob cmdlet, which uses threads for concurrency, but otherwise seamlessly integrates with the other *-Job cmdlets (note that it has no short-hand syntax):
$j = Start-ThreadJob { sleep 2; 'hi' }
[Not advisable] You can use something like Start-Process -NoNewWindow powershell -Args ..., but it is ill-advised:
Passing a shell command to powershell via Start-Process requires intricate quoting based on obscure rules - see this answer of mine for background information.
Any stdout and stderr output produced by the application / shell command will by default arrive asynchronously in the current console, interspersed with what you're doing interactively.
While you can redirect these streams to files with RedirectStandardOutput and -RedirectStandardError (and stdin input via -RedirectStandardInput) and you can use -PassThru to obtain a process-information object to determine the status and exit code of the process, Start-Job and Receive-Job are a simpler, more convenient alternative.
P.S.: I don't have an explanation for why cmd.exe "/c start notepad.exe" is blocking in the ISE but not in the regular console. Given the solutions above, however, getting to the bottom of this discrepancy may not be needed.