If I have a PowerShell module that acts as a wrapper over an executable program, and I would like to communicate failure to the parent process so that it can be checked programmatically, how would I best do this?
Possible ways that the module could be invoked from (non-exhaustive):
- From within the PowerShell shell (powershell or pwsh),
- From the command prompt (.e.g as
powershell -Command \<module fn\>
), - From an external program creating a PowerShell process (e.g. by calling
powershell -Command \<module fn\>
)).
If I throw an exception from the module when the executable fails, say, with
if ($LastExitCode -gt 0) { throw $LastExitCode; }
it appears to cover all of the requirements. If an exception is thrown and the module was called
- from within the PowerShell shell,
$?
variable is set toFalse
. - from the command prompt, the
%errorlevel%
variable is set to1
. - from an external process, the
exit
code is set to1
.
Thus the parent process can check for failure depending on how the module was called.
A small drawback with this approach is that the full range of exit codes cannot be communicated to the parent process (it either returns True/False in $?
or 0/1 as the exit
code), but more annoyingly the exception message displayed on the output is too verbose for some tastes (can it be suppressed?):
if ($LastExitCode -gt 0) { throw $LastExitCode; }
~~~~~~~~~~~~~~~~~~~
CategoryInfo : OperationStopped: (9:Int32) [], RuntimeException
FullyQualifiedErrorId : 9
Are there any better ways to communicate failure of an executable invoked from a PowerShell module to the parent process?
Thanks
CodePudding user response:
The PowerShell exit
keyword has an optional parameter. If that parameter is an integer, it's used as the process exit code. This way you can propagate the error code of the wrapped executable.
An example of Python capturing PowerShell's exit code:
:~> py
Python 3.7.9 [...]
>>> from subprocess import run
>>> res = run('powershell -command "exit 123"')
>>> res.returncode
123
CodePudding user response:
In general, well-behaved PowerShell scripts are not expected to throw, unless told so (explicitly by passing common parameter -ErrorAction Stop
or implicitly using $ErrorActionPreference = 'Stop'
).
To support this behavior and get the ability to pass additional error context information, you may use $PSCmdlet.WriteError()
, which becomes available when you declare your script to be an advanced-function cmdlet. This is simply done by inserting [CmdletBinding()]
attribute in front of the param(…)
block (you may use an empty param()
block if you don't need parameters).
TestProcessExitCode.ps1
[CmdletBinding()] param()
# As an example, call cmd.exe with a script that returns exit code 42
$processPath = $env:COMSPEC
& $processPath /c exit /b 42
# Make sure to check for inequality, because $LASTEXITCODE may be negative!
if( $LASTEXITCODE -ne 0 ) {
# Logs a non-terminating error or throws (e. g. if -EA Stop is passed)
$PSCmdlet.WriteError( [Management.Automation.ErrorRecord]::new(
[Exception]::new("Process failed with exit code $LASTEXITCODE : `"$processPath`""),
'ProcessFailure',
[Management.Automation.ErrorCategory]::WriteError,
[PSCustomObject]@{ ExitCode = $LASTEXITCODE; ProcessPath = $processPath }
))
}
# Make the exit code available to the parent process
exit $LASTEXITCODE
Usage from PowerShell, causing a non-terminating error:
# Assuming the default $ErrorActionPreference = 'Continue'
.\TestProcessExitCode.ps1
$LASTEXITCODE # Outputs 42
Usage from PowerShell, causing a terminating error:
try {
# Common parameter -ErrorAction (-EA) turns non-terminating errors into terminating ones
.\TestProcessExitCode.ps1 -EA Stop
}
catch {
# Output the error message
$_.Exception.Message
# Access to the individual error information
$_.TargetObject.ProcessPath
$_.TargetObject.ExitCode
}
Usage from cmd shell:
powershell -File .\TestProcessExitCode.ps1
echo %ERRORLEVEL% # Outputs 42
Now we get all possible error information, but the error output is somewhat verbose (unless catched by another PowerShell script, as in the example above):
TestProcessExitCode.ps1 : Process failed with exit code 42 :
"C:\WINDOWS\system32\cmd.exe"
CategoryInfo : WriteError: (@{ExitCode=42; ...stem32\cmd.exe}:PSObject) [TestProcessExitCode.ps1], Exc
eption
FullyQualifiedErrorId : ProcessFailure,TestProcessExitCode.ps1
In PowerShell Core 7 , we can set $ErrorView = ConciseView
at the beginning of the PowerShell script to produce a nice one-line error message:
TestProcessExitCode.ps1: Process failed with exit code 42 : "C:\WINDOWS\system32\cmd.exe"
For PowerShell 5, there is only $ErrorView = CategoryView
to reduce verbosity:
WriteError: (@{ExitCode=42; ...stem32\cmd.exe}:PSObject) [TestProcessExitCode.ps1], Exception
Unfortunately this looks even more cryptic than the full error message. As a workaround you might want to add a [switch]
parameter to your script to either call $PSCmdlet.WriteError()
for use from other PowerShell scripts or simply output the error message as a single text line, for use from cmd
shell.