【问题标题】:PowerShell: detecting errors in script functionsPowerShell:检测脚本函数中的错误
【发布时间】:2017-11-14 05:44:48
【问题描述】:

检测脚本函数中是否发生错误的最佳方法是什么?我正在寻找一种一致的方式来指示类似于 $? 的错误/成功状态(仅适用于 cmdlet,不适用于脚本函数)。

给定一个特定的函数可能会返回一个供调用者使用的值,我们不能通过返回一个布尔值来表示成功。函数可以使用 [ref] 参数并在函数内部适当地设置值并在调用后检查,但这比我想要的开销更大。我们可以使用 PowerShell 内置的东西吗?

我能想到的最好的是:

  1. 在函数中使用 Write-Error 来 将 ErrorRecord 对象放入错误中 流;
  2. 调用函数 错误变量参数;
  3. 检查ErrorVariable参数是否 调用后不为空。

例如:

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

还有更好的吗?有没有使用$的方法?在我们的脚本函数中?我宁愿不抛出异常或 ErrorRecord 对象,并且到处都有大量的 try/catch 块。我也不想使用 $Error ,因为这需要在调用函数之前检查计数,因为在调用之前可能存在其他错误 - 我不想 Clear() 并丢失它们。

【问题讨论】:

  • 你为什么不想使用try/catch?它并没有比if 语句增加多少; try{}catch{}if($err){} 仅相差 2 个字符。
  • 我想做的是:foo -ev Err if ($? -eq $false) { ReportError $Err } 在 try/catch 中包装每个函数调用让我分心;使用if (...) { ReportError ... } 跟踪每个函数调用似乎更干净。如果我真的无法捕获/防止终止错误,我宁愿只在 try/catch 中包装代码。
  • 想想dir nodrivefound:\ ErrorRecords被放到错误流中,可以用2>或者-ErrorVariable, $?是 $false,因此很容易检测到错误,但脚本不会终止,也没有 try/catch。我希望有这样的东西。

标签: powershell powershell-2.0


【解决方案1】:

检测脚本函数中是否发生错误的最佳方法是什么?我正在寻找一种一致的方式来指示类似于 $? 的错误/成功状态(仅适用于 cmdlet,不适用于脚本函数)。

PowerShell 中的错误处理一团糟。有错误记录、脚本异常、.NET异常、$?$LASTEXITCODEtraps、$Error数组(作用域之间)等等。并构造使这些元素相互交互(例如$ErrorActionPreference)。当你有这样的泥潭时,很难保持一致;但是,有一种方法可以实现这一目标。

必须注意以下几点:

  • $? 是一个未被充分记录的谜团。 $? 来自 cmdlet 调用的值不会传播,它是一个“只读变量”(因此不能手动设置)并且不清楚它何时被设置(可能是一个“执行状态”,除了在about_Automatic_Variables 中对$? 的描述之外,从未在PowerShell 中使用过这个术语,这是一个谜)。值得庆幸的是,布鲁斯·帕耶特已经阐明了这一点:如果你想设置 $?$PSCmdlet.WriteError() 是唯一已知的方法。

  • 如果您希望函数像 cmdlet 一样设置 $?,则必须避免使用 Write-Error,而是使用 $PSCmdlet.WriteError()Write-Error$PSCmdlet.WriteError() 做同样的事情,但前者没有正确设置 $? 而后者可以。 (不要费心试图在某处找到此文档。事实并非如此。)

  • 如果您想正确处理 .NET 异常(好像它们是非终止错误,将暂停整个执行的决定留给客户端代码),您必须 catch$PSCmdlet.WriteError() 他们.您不能不处理它们,因为它们会变成非终止错误,尊重$ErrorActionPreference。 (也没有记录。)

换句话说,产生一致的错误处理行为的关键是尽可能使用$PSCmdlet.WriteError()。它设置$?,尊重$ErrorActionPreference(因此-ErrorAction)并接受从其他cmdlet 或catch 语句(在$_ 变量中)生成的System.Management.Automation.ErrorRecord 对象。

以下示例将展示如何使用此方法。

# 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])
}

最后一点,如果您想从 .NET 异常中创建终止错误,请执行 try/catch 和 rethrow 捕获的异常。

【讨论】:

  • 我无法让@alek 解决方案发挥作用。设置 -ErrorAction:SilentlyContinue 只是忽略错误。这可能是因为 MS 在 PS 5.1 中改变了一些东西吗?
【解决方案2】:

听起来您正在寻找一种通用机制来记录从脚本调用的命令中发生的任何错误。如果是这样,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

运行此测试脚本会产生以下输出,而无需对调用的函数进行任何修改:

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

如果在报告错误后脚本执行应该停止,请将陷阱处理程序中的continue 替换为break

【讨论】:

  • 随着 continue 在那里,在异常信息和 begin 3 行之间不应该有一行说 `end 2` 吗?如果没有,为什么不呢?
  • @CodeJockey trap 在脚本中,不在函数中,所以在函数退出后,continue 在外部脚本中执行。如果你将trap 块移到函数中,那么你将得到你描述的行为。
【解决方案3】:

想到两件事: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)
   }
}

恕我直言,通常最好让函数本身尽可能地处理其错误。

【讨论】:

  • (请忽略最后一条评论)如果可能,我想避免抛出异常并尝试/捕获块。理想情况下,这些函数应该像 cmdlet 一样运行 - 将 ErrorRecords 添加到可以处理/过滤/忽略/等的错误流中。以许多不同的方式,但是调用者决定。除非明确捕获,否则抛出异常似乎很苛刻,因为它会终止当前函数和脚本执行。我更喜欢 $?... 的简单优雅。
  • Cmdlet 抛出异常,最终出现在错误流中。所以抛出异常就像 Cmdlet 一样。异常是发出命令失败信号的正确方法。
  • 如果您要重新抛出异常,请使用throw $_,这样您就不会吃掉堆栈跟踪或其他有价值的信息。
【解决方案4】:

我相信你想要一个全局变量 $GLOBAL:variable_name。该变量将在脚本的范围内,而不仅仅是函数。

查看您可能还想使用陷阱 (Get-Help about_Trap) 的代码 - 尽管上面的 $GLOBAL:variable_name 可以与您的代码一起使用。这是代码示例的重新编写 - 我没有对此进行测试,所以它更像是伪代码...... :)

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,马特

【讨论】:

    【解决方案5】:

    $?取决于函数是否抛出终止错误。如果使用 Write-Error,而不是 Throw,$?未设置。许多 cmdlet 不设置 $?当他们有错误时,因为该错误不是终止错误。

    设置函数的最简单方法 $?是使用 -ErrorAction 停止。这将在您的函数出错时停止脚本,并且 $?将被设置。

    注意这块样例怎么看$?作品:

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

    希望对你有帮助

    【讨论】:

    • -ErrorAction 停止并抛出做工作 - 他们设置 $?到 $false - 但是 $?之后在您的示例中永远不会显示,因为脚本由于 -ErrorAction Stop 或 throw 而终止。如果 foo 调用包含在 try/catch 中,它可以工作,但我希望避免必须将每个函数调用都包含在 try/catch 中。如果你使用 try/catch 没有必要检查 $?因为如果你在 catch 块中,就会发生错误。
    • 我想做的如下 - 它很干净但可以处理所有错误:foo -ErrorVariable Err if ($? -eq $false) { ReportError $Err } 您还可以轻松地将函数名称和参数(如果使用飞溅)传递给 ReportError。
    【解决方案6】:

    大部分都在我头顶上方发出可爱的嗖嗖声...ಠ_ಠ

    我和丹在一起。 PS Logging 完全是一团糟,看起来它会使我正在编写的代码的大小增加一倍以上......

    坦率地说,如果我可以直接将控制台输出捕获到日志、疣和所有内容中,我会很高兴...

    Try/Catch 块太……太……糟透了,我能闻到它,它让我的眼睛变成棕色。

    $?很有趣,但你们实际上知道自己在做什么,就像我现在的情况一样,我意识到我什么都不知道(上周我以为我至少知道一些东西,但不知道)。

    为什么 %$#@%$ 在 cli 中没有类似 2> 的东西...

    好的,这就是我想要做的(你已经读到这里了,为什么不呢?):

        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
    } 
    

    成功了吗?是的,很好...记录并继续。不?然后捕获错误,记录并继续脚本...

    我很想只要求用户输入、添加内容并继续...

    顺便说一句,我确实找到了一个模块 PSLogging,它看起来很酷,但我不知道如何让它工作......文档有点简陋。似乎人们可以毫不费力地让它工作,所以我觉得我是一个坐在角落里的尖尖帽子的人......

    【讨论】:

      猜你喜欢
      • 2022-01-01
      • 1970-01-01
      • 2012-03-20
      • 1970-01-01
      • 2015-08-02
      • 2010-12-23
      • 2023-03-26
      • 2018-02-21
      • 2018-03-25
      相关资源
      最近更新 更多