简体   繁体   English

PowerShell:检测脚本函数中的错误

[英]PowerShell: detecting errors in script functions

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). (仅适用于cmdlet,而不适用于脚本函数)。

Given a particular function might return a value to be used by the caller, we can't indicate success by returning a boolean. 给定特定函数可能返回调用者要使用的值,我们不能通过返回布尔值来指示成功。 A function could use a [ref] parameter and set the value appropriately inside the function and check after the call, but this is more overhead than I'd like. 函数可以使用[ref]参数并在函数内部设置适当的值并在调用后进行检查,但这比我想要的开销更大。 Is there something built-in to PowerShell that we can use? 我们可以使用PowerShell内置的东西吗?

The best I can come up with is: 我能想到的最好的是:

  1. In the function use Write-Error to put ErrorRecord objects in the error stream; 在函数中使用Write-Error将ErrorRecord对象放入错误流中;
  2. call the function with an ErrorVariable parameter; 使用ErrorVariable参数调用该函数;
  3. check if the ErrorVariable parameter is not null after the call. 检查调用后ErrorVariable参数是否为空。

For example: 例如:

function MyFun {
  [CmdletBinding()]    # must be an advanced function or this 
  param ()             # will not work as ErrorVariable will not exist
  process {
    # code here....
    if ($SomeErrorCondition) {
      Write-Error -Message "Error occurred; details..."
      return
    }
    # more code here....
  }
}

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

Is there something better? 还有更好的东西吗? Is there a way of using $? 有没有办法使用$? in our script functions? 在我们的脚本函数中? I'd rather not throw exceptions or ErrorRecord objects and have tons of try/catch blocks all over the place. 我宁愿不抛出异常或ErrorRecord对象,并且在整个地方都有大量的try / catch块。 I'd also rather not use $Error as would require checking the count before making the function call as there could be other errors in there before the call - and I don't want to Clear() and lose them. 我还宁愿不使用$ Error,因为在调用函数之前需要检查计数,因为在调用之前可能还有其他错误 - 而且我不想Clear()并丢失它们。

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). (仅适用于cmdlet,而不适用于脚本函数)。

Error handling in PowerShell is a total mess. PowerShell中的错误处理完全是一团糟。 There are error records, script exceptions, .NET exceptions, $? 有错误记录,脚本异常,.NET异常, $? , $LASTEXITCODE , trap s, $Error array (between scopes), and so on. $LASTEXITCODEtrap s, $Error数组(在范围之间),依此类推。 And constructs to interact these elements with each other (such as $ErrorActionPreference ). 并构造相互交互这些元素(例如$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). 从cmdlet的通话值不传播,这是一个“只读变量”(因而无法通过手动设置),它是不是当它到底被设置(怎么可能是一个“执行状态”上明确,长期从未使用过在PowerShell中,除了about_Automatic_Variables$?的描述about_Automatic_Variables ,是一个谜。 Thankfully Bruce Payette has shed light on this: if you want to set $? 值得庆幸的是Bruce Payette对此有所了解:如果你想设置$? , $PSCmdlet.WriteError() is the only known way. $PSCmdlet.WriteError()是唯一已知的方法。

  • If you want functions to set $? 如果你想要函数设置$? as cmdlets do, you must refrain from Write-Error and use $PSCmdlet.WriteError() instead. 如cmdlet所做,您必须避免Write-Error并使用$PSCmdlet.WriteError()代替。 Write-Error and $PSCmdlet.WriteError() do the same thing, but the former does not set $? Write-Error$PSCmdlet.WriteError()做同样的事情,但前者没有设置$? 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. 如果你想正确处理.NET异常(就好像它们是非终止错误,将整个执行暂停到客户端代码的决定),你必须catch$PSCmdlet.WriteError()它们。 You cannot leave them unprocessed, since they become non-terminating errors which do not respect $ErrorActionPreference . 你不能离开他们未加工的,因为他们成为尊重非终止错误$ErrorActionPreference (Not documented either.) (也没有记录。)

In other words, the key to produce consistent error handling behavior is to use $PSCmdlet.WriteError() whenever possible. 换句话说,产生一致的错误处理行为的关键是尽可能使用$PSCmdlet.WriteError() 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). ,尊重$ErrorActionPreference (以及-ErrorAction )并接受从其他cmdlet或catch语句(在$_变量中)生成的System.Management.Automation.ErrorRecord对象。

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 re throw the exception caught. 最后要注意的是,如果要从.NET异常中创建终止错误,请try / catch并重新throw捕获的异常。

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: 如果是这样, trap可能是最合适的机制:

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. 如果在报告错误后脚本执行应该停止,请在陷阱处理程序中将continue替换为break

Two things come into mind: Throw (better than Write-Error in your example above), and try..catch 我想到两件事: Throw (比上面例子中的Write-Error更好)和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. Imho,通常最好让函数本身尽可能地处理它的错误。

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... PS记录是一个完整的混乱,似乎它将超过我正在编写的代码的大小的两倍...

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. Try / Catch块是如此......如此......蹩脚,我能闻到它,它让我的眼睛变成棕色。

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). 非常有趣,但是你们真的知道你在做什么,就像我已经意识到我什么都不知道的那样(上周我以为我至少知道一些东西,但是noooooo)。

Why the %$#@%$ isn't there something like the 2> in cli ... 为什么%$#@%$与cli中的2>不一样...

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. 顺便说一句,我确实找到了一个模块PSLogging,看起来很酷,但我不知道如何让它工作......文档有点斯巴达。 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... 好像人们正在努力工作而没有太多麻烦,所以我觉得我是一个角落里坐着尖帽子的人......

$? $? depends on if the function throw a terminating error or not. 取决于函数是否抛出终止错误。 If Write-Error is used, not Throw, $? 如果使用Write-Error,不是Throw,$? is not set. 没有设定。 Many cmdlets don't set $? 许多cmdlet没有设置$? 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. 是使用-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. 我相信你想要一个全局变量$ 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. 查看代码,您可能也想使用陷阱( Get-Help about_Trap ) - 虽然$ GLOBAL:variable_name可以与您的上面一起工作。 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 HTH,马特

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM