Home > Software engineering >  How to best indicate failure of a PowerShell module to the calling shell/process?
How to best indicate failure of a PowerShell module to the calling shell/process?

Time:10-30

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):

  1. From within the PowerShell shell (powershell or pwsh),
  2. From the command prompt (.e.g as powershell -Command \<module fn\>),
  3. 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

  1. from within the PowerShell shell, $? variable is set to False.
  2. from the command prompt, the %errorlevel% variable is set to 1.
  3. from an external process, the exit code is set to 1.

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.

  • Related