Is it possible to force PowerShell script to throw if a required *pipeline* parameter is omitted?

℡╲_俬逩灬. 提交于 2019-12-22 09:46:11

问题


Interactive PowerShell sessions prompt the user when a required parameter is omitted. Shay Levy offers a workaround to this problem. The problem is that workaround does not work when you use the pipeline to bind parameters.

Consider this example:

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

This throws an exception despite that $o.a is a perfectly good value to bind to f -a. For some reason PowerShell evaluates the default value for parameter $a even if there is a value for $a that is destined to be bound from the pipeline.

Is there some other way to force PowerShell to throw an exception when a mandatory parameter is missing when running interactively?


Why does this matter? It wastes programmer time. Here's how:

  • It's pretty normal for the stack trace to be 20 calls deep. When a call deep in the call stack blocks because it didn't receive a mandatory parameter things become very inefficient to debug. There is no stack trace, no error messages, and no context. All you see is a prompt for the parameter value. Good luck guessing exactly why that occurred. You can always debug your way to a solution, it just takes way more time than it should because you're not getting the information you normally would from a thrown exception.

  • Suppose you are running a series of configuration test cases and one of 1000 has this problem. On average, 500 of those test cases don't run. So you only get test results from half of your cases on this test run. If those test runs were running overnight, you might have to wait another 24 hours to get the results. So now you're iterating slower.


回答1:


Preface

All of the solutions I have seen are mere workarounds to this fundamental problem: In non-interactive mode PowerShell throws an exception when a parameter is missing. In interactive mode, there is no way to tell PowerShell to throw an exception in the same way.

There really ought to be an issue opened on Connect for this problem. I haven't been able to do a proper search for this on Connect yet.

Use the Pipeline to Bind Parameters

As soon as you involve the pipeline for parameter binding in any way, missing parameters produce an error. And, if $ErrorActionPreference -eq 'Stop' it throws an exception:

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

That throws ParameterBindingException for parameter b because it is mandatory. Note that there's some bizarreness related to catching that exception under PowerShell 2.

I tested a few different variations using piped and splatted parameters, and it looks like involving pipeline binding in any way avoids prompting the user.

Unfortunately, this means creating a parameters object every time you invoke a command where parameters might be missing. That normally involves a rather verbose call to New-Object psobject -Property @{}. Since I expect to use this technique often, I created ConvertTo-ParamObject (and alias >>) to convert splat parameters to a parameter object. Using >> results in code that looks something like this:

$UnvalidatedParams | >> | f

Now suppose $UnvalidatedParams is a hashtable that came from somewhere that may have omitted one of f's mandatory parameters. Invoking f using the above method results in an error instead of the problematic user prompt. And if $ErrorActionPreference is Stop, it throws an exception which you can catch.

I've already refactored a bit of code to use this technique, and I'm optimistic that this is the least-bad workaround I've tried. @Briantist's technique is really rather clever, but it doesn't work if you can't change the cmdlet you are invoking.




回答2:


The reason this doesn't work is that pipeline parameters have different values depending on whether you're in the Begin {}, Process {}, or End {} block. At some point the default gets evaluated so an exception will be thrown. This is one reason I don't like that particular hack.

A Suitable Solution (I hope)

I liked it so much I wrote a blog post about it so I hope you find it useful.

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

I think the biggest advantage to this is that it gets called the same way no matter how many parameters you have or what their names are.

The key is that you only have to add an alias to each parameter that begins with Required_.

Example:

function f {
[CmdletBinding()]
param(
    [Parameter(
        ValueFromPipeline=$true
    )]
    [Alias('Required_Param1')]
    $Param1
)

    Process {
        $PSBoundParameters | Validate-MandatoryOptionalParameters -Context $MyInvocation.MyCommand
    }
}

Based on our chat conversation and your use case, I messed around with setting a breakpoint instead of throwing. Seems like it could be useful, but not certain. More info in the post.

Also available as a GitHub Gist (which includes comment-based help).


I think the only way you're going to get around this is to check the value in your process block.

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

if you control the invocation of your script, you can use powershell.exe -NonInteractive and that should throw (or at least exit) instead of prompting.

Validation Function Example

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
}



回答3:


First if your function should accept a value from pipeline you need to declare it in "Parameter" by ValueFromPipeline=$True. Why PS evaluate first the default value for the parameter? I don't have an explanation.

But you can always use If statement inside of your function to evaluate if the parameter is empty and generate an error if it is true.

Try this:

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 



回答4:


Have you tried ValidateScript?

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



回答5:


Start powershell in non-interactive mode:

powershell -NonInteractive


来源:https://stackoverflow.com/questions/33600279/is-it-possible-to-force-powershell-script-to-throw-if-a-required-pipeline-para

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