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.
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.
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_
.
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.
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
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With