【问题标题】:Is it possible to force PowerShell script to throw if a required *pipeline* parameter is omitted?如果省略必需的 *pipeline* 参数,是否可以强制 PowerShell 脚本抛出?
【发布时间】:2016-02-09 13:50:31
【问题描述】:

当省略必需参数时,交互式 PowerShell 会话会提示用户。 Shay Levy offers a workaround 这个问题。问题是当您使用管道绑定参数时,解决方法不起作用。

考虑这个例子:

function f {
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeLineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$a=$(throw "a is mandatory, please provide a value.")
    )
    process{}
}

$o = New-Object psobject -Property @{a=1}
$o | f

尽管$o.a 是绑定到f -a 的完美值,但这会引发异常。出于某种原因,即使 $a 的值注定要从管道绑定,PowerShell 也会评估参数 $a 的默认值。

在交互式运行时缺少强制参数时,是否有其他方法可以强制 PowerShell 抛出异常?


为什么这很重要?它浪费了程序员的时间。方法如下:

  • 堆栈跟踪深度为 20 次调用是很正常的。当调用堆栈深处的调用因为没有接收到强制参数而阻塞时,调试变得非常低效。没有堆栈跟踪,没有错误消息,也没有上下文。您所看到的只是参数值的提示。祝你好运,准确地猜出发生这种情况的原因。您始终可以通过调试方式找到解决方案,只是需要的时间比应有的要多得多,因为您无法从抛出的异常中获得通常会得到的信息。

  • 假设您正在运行一系列配置测试用例,而 1000 个中的一个 存在此问题。平均而言,这些测试用例中有 500 个没有运行。因此,在此测试运行中,您只能从一半的案例中获得测试结果。如果这些测试运行在一夜之间运行,您可能需要再等 24 小时才能获得结果。所以现在你的迭代速度变慢了。

【问题讨论】:

  • 感谢您发布您的解释。我确实想指出,如果您使用[Parameter(HelpMessage="I am function-whatever, I need this parameter")],该消息将与提示一起显示(默认情况下它不会显示在帮助中,因此实际上这就是它的全部好处)。
  • ????提示: $o = [pscustomobject]@{a=1}$o = New-Object psobject -Property @{a=1} 做同样的事情

标签: powershell parameters pipeline interactive


【解决方案1】:

我已经尝试了多种上述方法,但它们都未能做到在缺少参数时应向用户提供合理消息的简单要求。使用 ValidateScript Powershell 总是会在我自己的消息之前添加“无法验证参数 'fileName' 的参数”,这是我不想要的。

我最终去了老学校,只是这样做:

param(
    [Parameter()] 
    [string]$fileName=""
)

if([string]::IsNullOrEmpty($fileName)) {
    throw [System.ArgumentException] "File name is required."
}

这行得通。我得到的唯一消息是“需要文件名”。

【讨论】:

    【解决方案2】:

    以非交互模式启动powershell:

    powershell -NonInteractive
    

    【讨论】:

      【解决方案3】:

      前言

      我看到的所有解决方案都只是解决这个基本问题的方法:在非交互模式下,当缺少参数时,PowerShell 会引发异常。在交互模式下,没有办法告诉 PowerShell 以同样的方式抛出异常。

      确实应该在Connect 上针对这个问题打开一个问题。我还不能在 Connect 上正确搜索此内容。

      使用管道绑定参数

      只要您以任何方式涉及参数绑定的管道,缺少参数就会产生错误。而且,如果$ErrorActionPreference -eq 'Stop' 会引发异常:

      function f {
          [CmdletBinding()]
          param
          (
              [Parameter(Mandatory = $true,
                         ValueFromPipeLineByPropertyName=$true)]
              [ValidateNotNullOrEmpty()]
              [string]$a,
      
              [Parameter(Mandatory = $true ,
                         ValueFromPipeLineByPropertyName=$true)]
              [ValidateNotNullOrEmpty()]
              [string]$b,
      
              [Parameter(ValueFromPipeLineByPropertyName=$true)]
              [ValidateNotNullOrEmpty()]
              [string]$c
          )
          process{}
      }
      
      $o = New-Object psobject -Property @{a=1}
      $splat = @{c=1}
      $o | f @splat
      

      这会为参数b 抛出ParameterBindingException,因为它是强制性的。注意there's some bizarreness related to catching that exception under PowerShell 2

      不幸的是,这意味着每次调用可能缺少参数的命令时都会创建一个参数对象。这通常涉及对New-Object psobject -Property @{} 的相当冗长的调用。因为我希望经常使用这种技术,所以我创建了ConvertTo-ParamObject(和别名>>)来将 splat 参数转换为参数对象。使用 >> 会生成如下所示的代码:

      $UnvalidatedParams | >> | f
      

      现在假设$UnvalidatedParams 是一个来自某个地方的哈希表,它可能省略了f 的强制参数之一。使用上述方法调用f 会导致错误而不是有问题的用户提示。如果$ErrorActionPreferenceStop,它会抛出一个你可以捕获的异常。

      我已经重构了一些代码来使用这种技术,我很乐观地认为这是我尝试过的最不坏的解决方法。 @Briantist 的 technique 确实很聪明,但如果你不能更改你正在调用的 cmdlet,它就不起作用了。

      【讨论】:

        【解决方案4】:

        这不起作用的原因是管道参数具有不同的值,具体取决于您是在 Begin {}Process {} 还是 End {} 块中。在某些时候,默认值会被评估,因此将引发异常。这也是我不喜欢那个特别的 hack 的原因之一。

        一个合适的解决方案(我希望)

        I liked it so much I wrote a blog post about it 所以希望对你有用。

        function Validate-MandatoryOptionalParameters {
        [CmdletBinding()]
        param(
            [Parameter(
                Mandatory=$true
            )]
            [System.Management.Automation.CommandInfo]
            $Context ,
        
            [Parameter(
                Mandatory=$true,
                ValueFromPipeline=$true
            )]
            [System.Collections.Generic.Dictionary[System.String,System.Object]]
            $BoundParams ,
        
            [Switch]
            $SetBreakpoint
        )
        
            Process {
                foreach($param in $Context.Parameters.GetEnumerator()) {
                    if ($param.Value.Aliases.Where({$_ -imatch '^Required_'})) {
                        if (!$BoundParams[$param.Key]) {
                            if ($SetBreakpoint) {
                                $stack = Get-PSCallStack | Select-Object -Index 1
                                Set-PSBreakpoint -Line $stack.ScriptLineNumber -Script $stack.ScriptName | Write-Debug
                            } else {
                                throw [System.ArgumentException]"'$($param.Key)' in command '$($Context.Name)' must be supplied by the caller."
                            }
                        }
                    }
                }
            }
        }
        

        我认为这样做的最大优势在于,无论您有多少参数或它们的名称是什么,它都会以相同的方式调用。

        关键是你只需要为每个以Required_开头的参数添加一个别名。

        示例:

        function f {
        [CmdletBinding()]
        param(
            [Parameter(
                ValueFromPipeline=$true
            )]
            [Alias('Required_Param1')]
            $Param1
        )
        
            Process {
                $PSBoundParameters | Validate-MandatoryOptionalParameters -Context $MyInvocation.MyCommand
            }
        }
        

        根据我们的聊天对话和您的用例,我搞砸了设置断点而不是抛出。似乎它可能有用,但不确定。更多信息在帖子中。

        也可作为GitHub Gist 提供(包括基于评论的帮助)。


        我认为解决此问题的唯一方法是检查进程块中的值。

        Process {
            if (!$a) {
                throw [System.ArgumentException]'You must supply a value for the -a parameter.'
            }
        }
        

        如果您控制脚本的调用,则可以使用 powershell.exe -NonInteractive 并且应该抛出(或至少退出)而不是提示。

        验证函数示例

        function Validate-Parameter {
        [CmdletBinding()]
        param(
            [Parameter(
                Mandatory=$true , #irony
                ValueFromPipeline=$true
            )]
            [object]
            $o ,
        
            [String]
            $Message
        )
        
            Begin {
                if (!$Message) {
                    $Message = 'The specified parameter is required.'
                }
            }
        
            Process {
                if (!$o) {
                    throw [System.ArgumentException]$Message
                }
            }
        }
        
        # Usage
        
        Process {
            $a | Validate-Parameter -Message "-a is a required parameter"
            $a,$b,$c,$d | Validate-Parameter
        }
        

        【讨论】:

        • 我担心你会这么说。那是一大堆额外的代码行,只是为了获取异常而不是挂起。
        • @alx9r 刚刚添加了一个可能有用的编辑,具体取决于您在做什么。
        • 感谢您的加入。我发现-NonInteractive 仅适用于稀少的少数脚本。我发现大多数脚本至少在某些时候需要以交互方式运行。例如,如果您需要 RunAs 凭据,则以交互方式提供它们通常是最实用的解决方案。但是你有时会遇到这个问题:(
        • @alx9r 同意;我真的只将它用于计划任务。我唯一能提供的另一件事是将验证包装在一个函数中,然后在每个参数上调用它。 $a | Validate-Parameter。即使使用管道代码,该功能也会很小。它只会做同样的检查,如果它是好的,它不会返回任何东西,如果不是,它会返回 throw
        • 这可能是最不坏的选择。这样做的缺点是您混淆了该参数实际上是来自Get-Help 的强制参数,以及依赖于该 Cmdlet 强制参数的反射的任何东西。我认为删除Mandatory=$true 也可能会影响参数集分辨率。我希望[parameter(NeverPrompt=$true)] 是一件事。
        【解决方案5】:

        你试过ValidateScript吗?

        [ValidateScript({
          if ($_ -eq $null -or $_ -eq '') {
            Throw "a is mandatory, please provide a value."
          }
          else {
            $True
          }
        })]
        

        【讨论】:

        • 验证器仅在绑定参数(由调用者显式提供的参数)上调用,因此对于缺少的参数,它们将永远不会被调用。
        • 尽管 alx9r 指出他的问题是缺少参数,但他的示例似乎是关于让 PowerShell 识别实际上并未丢失的参数。
        • 是的,确实如此;他的参数没有丢失。问题是仍在应用默认值(在与他使用变量时不同的上下文中),目前默认值是他检查它是否丢失的唯一方法。但是你试过了吗? [ValidateScript()]除非参数已经绑定,否则不会被调用,所以不能用来查找未绑定的参数。
        【解决方案6】:

        首先,如果您的函数应该接受来自管道的值,您需要通过ValueFromPipeline=$True 在“参数”中声明它。为什么 PS 首先评估参数的默认值?我没有解释。

        但您始终可以在函数内部使用 If 语句来评估参数是否为空,如果为真则生成错误。

        试试这个:

        function f {
            [CmdletBinding()]
            param
            (
                [Parameter(ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True)]
                [ValidateNotNullOrEmpty()]
                [string]$a
            )
            process{
        
                if (!($a)) {
                    $(throw "a is mandatory, please provide a value.")
                }
            }
        }
        
        $o = New-Object psobject -Property @{a=1}
        $o.a| f 
        

        【讨论】:

        • 我不明白您在 ValueFromPipeLine 和 ValueFromPipeLineByPropertyName 之间所做的区别是如何相关的。他们都遇到同样的问题。
        • ValueFromPipelineValueFromPipelineByPropertyName 做不同的事情。使用其中一个或另一个,或两者,或两者都不使用是有效的,并且每次的行为都不同。后者从与参数名称(或其别名之一)共享的管道对象的属性中检索值,而前者获取对象本身的值。这就是'C:\' | Get-Item(Get-Process)[0] | Get-Item 之间的区别。
        猜你喜欢
        • 1970-01-01
        • 2014-03-21
        • 1970-01-01
        • 2013-02-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-09
        相关资源
        最近更新 更多