PowerShell: detecting errors in script functions

不羁岁月 提交于 2019-11-30 05:22:19

What is the best way to detect if an error occurs in a script function? I'm looking for a consistent way to indicate error/success status similar to $? (which only works on cmdlets, not script functions).

Error handling in PowerShell is a total mess. There are error records, script exceptions, .NET exceptions, $?, $LASTEXITCODE, traps, $Error array (between scopes), and so on. And constructs to interact these elements with each other (such as $ErrorActionPreference). It is very difficult to get consistent when you have a morass like this; however, there is a way to achieve this goal.

The following observations must be made:

  • $? is an underdocumented mystery. $? values from cmdlet calls do not propagate, it is a "read-only variable" (thus cannot be set by hand) and it is not clear on when exactly it gets set (what could possibly be an "execution status", term never used in PowerShell except on the description of $? in about_Automatic_Variables, is an enigma). Thankfully Bruce Payette has shed light on this: if you want to set $?, $PSCmdlet.WriteError() is the only known way.

  • If you want functions to set $? as cmdlets do, you must refrain from Write-Error and use $PSCmdlet.WriteError() instead. Write-Error and $PSCmdlet.WriteError() do the same thing, but the former does not set $? properly and the latter does. (Do not bother trying to find this documented somewhere. It is not.)

  • If you want to handle .NET exceptions properly (as if they were non-terminating errors, leaving the decision of halting the entire execution up to the client code), you must catch and $PSCmdlet.WriteError() them. You cannot leave them unprocessed, since they become non-terminating errors which do not respect $ErrorActionPreference. (Not documented either.)

In other words, the key to produce consistent error handling behavior is to use $PSCmdlet.WriteError() whenever possible. It sets $?, respects $ErrorActionPreference (and thus -ErrorAction) and accepts System.Management.Automation.ErrorRecord objects produced from other cmdlets or a catch statement (in the $_ variable).

The following examples will show how to use this method.

# Function which propagates an error from an internal cmdlet call,
# setting $? in the process.
function F1 {
    [CmdletBinding()]
    param([String[]]$Path)

    # Run some cmdlet that might fail, quieting any error output.
    Convert-Path -Path:$Path -ErrorAction:SilentlyContinue
    if (-not $?) {
        # Re-issue the last error in case of failure. This sets $?.
        # Note that the Global scope must be explicitly selected if the function is inside
        # a module. Selecting it otherwise also does not hurt.
        $PSCmdlet.WriteError($Global:Error[0])
        return
    }

    # Additional processing.
    # ...
}


# Function which converts a .NET exception in a non-terminating error,
# respecting both $? and $ErrorPreference.
function F2 {
    [CmdletBinding()]
    param()

    try {
        [DateTime]"" # Throws a RuntimeException.
    }
    catch {
        # Write out the error record produced from the .NET exception.
        $PSCmdlet.WriteError($_)
        return
    }
}

# Function which issues an arbitrary error.
function F3 {
    [CmdletBinding()]
    param()

    # Creates a new error record and writes it out.
    $PSCmdlet.WriteError((New-Object -TypeName:"Management.Automation.ErrorRecord"
        -ArgumentList:@(
            [Exception]"Some error happened",
            $null,
            [Management.Automation.ErrorCategory]::NotSpecified,
            $null
        )
    ))

    # The cmdlet error propagation technique using Write-Error also works.
    Write-Error -Message:"Some error happened" -Category:NotSpecified -ErrorAction:SilentlyContinue
    $PSCmdlet.WriteError($Global:Error[0])
}

As a last note, if you want to create terminating errors from .NET exceptions, do try/catch and rethrow the exception caught.

It sounds like you are looking for a general mechanism to log any error occurring in a command called from a script. If so, trap is probably the most appropriate mechanism:

Set-Alias ReportError Write-Host -Scope script  # placeholder for actual logging

trap {
  ReportError @"
Error in script $($_.InvocationInfo.ScriptName) :
$($_.Exception) $($_.InvocationInfo.PositionMessage)
"@
  continue  # or use 'break' to stop script execution
}

function f( [int]$a, [switch]$err ) {
  "begin $a"
  if( $err ) { throw 'err' }
  "  end $a"
}

f 1
f 2 -err
f 3

Running this test script produces the following output, without requiring any modification to the called functions:

PS> ./test.ps1
begin 1
  end 1
begin 2
Error in script C:\Users\umami\t.ps1 :
System.Management.Automation.RuntimeException: err
At C:\Users\umami\t.ps1:13 char:21
+   if( $err ) { throw <<<<  'err' }
begin 3
  end 3

If script execution should stop after an error is reported, replace continue with break in the trap handler.

Two things come into mind: Throw (better than Write-Error in your example above), and try..catch

try
{
   #code here
}
catch
{
   if ($error[0].Exception -match "some particular error")
   {
       Write-Error "Oh No! You did it!"
   }
   else
   {
       Throw ("Ooops! " + $error[0].Exception)
   }
}

Imho, it is generally better to have the function itself to handle its errors as much as possible.

$? depends on if the function throw a terminating error or not. If Write-Error is used, not Throw, $? is not set. Many cmdlets don't set $? when they have an error, because that error is not a terminating error.

The easiest way to make your function set $? is to use -ErrorAction Stop. This will stop the script when your function errors, and $? will be set.

Note this block of samples to see how $? works:

function foo([ParameteR()]$p) { Write-Error "problem" } 

foo 

$?

foo -errorAction Stop



$?

function foo() { throw "problem" } 

foo 

$?

Hope this helps

I believe you want a global variable $GLOBAL:variable_name. That variable will be in the scope of the script not just the function.

Looking at the code you may want to use trap (Get-Help about_Trap) as well - though $GLOBAL:variable_name would work with yours above. Here's a re-wreite of the code example - I've not tested this so it's more pseudo-code... :)

function MyFun {
  [CmdletBinding()]    # must be an advanced function or this 
  param ()             # will not work as ErrorVariable will not exist
  begin {
    trap {
      $GLOBAL:my_error_boolean = $true
      $GLOBAL:my_error_message = "Error Message?"

      return
    }
  }
  process {
    # more code here....
  }
}

# call the function
$GLOBAL:my_error_boolean = $false
MyFun 
# this check would be similar to checking if $? -eq $false
if ($GLOBAL:my_error_boolean) {
  "An error was detected"
  # handle error, log/email contents of $Err, etc.
}

HTH, Matt

Most of this made a lovely whooshing sound as it went right over my head... ಠ_ಠ

I am with Dan. PS Logging is a complete mess and seems like it will more than double the size of the code I am writing...

Frankly, I would be happy if I could just capture console output directly to logs, warts and all...

The Try/Catch block is so ... so ... crappy, I can smell it and it has made my eyes turn brown.

The $? is very interesting, but you guys actually know what you are doing, as where I am at the point where I have realized I know nothing (last week I thought I knew at least something, but noooooo).

Why the %$#@%$ isn't there something like the 2> in cli ...

Ok so here is what I am trying to do (you've read this far, so why not?):

    Function MyFunc($Param1, $Param2){
Do{
  $Var = Get-Something | Select Name, MachineName, Status 
 $NotherVar = Read-Host -Prompt "Do you want to Stop or Start or check the $Var (1 to Start, 2 to stop, 3 to check, 4 to continue)?" 
    If ($SetState -eq 1) 
     {
      Do Stuff
    }
    ElseIf ($Var -eq 2) 
       {
      Do Stuff
    }
    ElseIf ($Var -eq 3)
       {
      Do Stuff
    }
  }
    Until ($Var -eq 4)
Do other stuff
} 

Did it work? Yes, fine... Log it and continue. No? Then catch the error, log it and continue the script...

I am tempted to just ask for user input, add-content and continue...

Incidentally, I did find a module PSLogging that seems like it would be pretty cool, but I am not sure how to get it working... The documentation is a bit Spartan. Seems like folks are getting it working without too much trouble, so I kinda feel like I am a corner sitting pointy hat kind of person...

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!