3

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.

alx9r
  • 3,675
  • 4
  • 26
  • 55
  • Thanks for posting your explanation. I do want to point out that if you use `[Parameter(HelpMessage="I am function-whatever, I need this parameter")]` that message will be displayed with the prompt (it doesn't get shown in help by default so that's actually about all it's good for). – briantist Nov 09 '15 at 02:33
  • ** Tip:** `$o = [pscustomobject]@{a=1}` does the same thing as `$o = New-Object psobject -Property @{a=1}` – brianary Oct 31 '18 at 20:10

6 Answers6

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
}
briantist
  • 45,546
  • 6
  • 82
  • 127
  • I was afraid you'd say this. That's a whole lot of extra lines of code just to get exceptions instead of hanging. – alx9r Nov 09 '15 at 00:59
  • @alx9r just added an edit that could be useful, depending on what you're doing. – briantist Nov 09 '15 at 01:08
  • Thanks for the addition. I have found that `-NonInteractive` is practical only for scarce few scripts. I'm finding that most scripts need to run interactively at least some of the time. If you need RunAs credentials, for example, providing them interactively is often the most practical solution. But then you run into this problem sometimes :( – alx9r Nov 09 '15 at 01:40
  • @alx9r agreed; I really only use it for scheduled tasks. The only other thing I can offer is that you wrap your validation in a function, then just call it on each parameter. `$a | Validate-Parameter`. The function would be small, even with the pipeline code. It would just do the same check and return nothing if it's good and `throw` if it's not. – briantist Nov 09 '15 at 01:43
  • That might turn out to be the least bad option. The downside to this is that you obfuscate that the parameter is, in fact mandatory from `Get-Help` and from anything relying on reflection of that Cmdlet's mandatory parameters. I think removing `Mandatory=$true` might also influence parameter set resolution. I wish `[parameter(NeverPrompt=$true)]` was a thing. – alx9r Nov 09 '15 at 01:50
  • @alx9r yeah removing `Mandatory=$true` on parameters that are supposed to be mandatory is not something I like to do. It definitely affects binding. But then I've never really understood the situation where prompting causes a problem beyond aesthetics, and I'd rather have the language features (help, sets, discovery via AST, etc.). – briantist Nov 09 '15 at 01:56
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/94561/discussion-between-alx9r-and-briantist). – alx9r Nov 09 '15 at 02:30
2

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.

alx9r
  • 3,675
  • 4
  • 26
  • 55
0

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 
kekimian
  • 909
  • 3
  • 13
  • 21
  • I don't see how the distinction you are making between ValueFromPipeLine and ValueFromPipeLineByPropertyName is relevant. They both suffer from the same problem. – alx9r Nov 09 '15 at 01:02
  • `ValueFromPipeline` and `ValueFromPipelineByPropertyName` do different things. It's valid to use one or the other, or both, or neither, and the behavior is different each time. The latter retrieves the value from a property of the pipeline object that shares the name of the parameter (or one of its aliases) whereas the former takes the value of the object itself. This is the difference between `'C:\' | Get-Item` and `(Get-Process)[0] | Get-Item`. – briantist Nov 09 '15 at 01:22
0

Have you tried ValidateScript?

[ValidateScript({
  if ($_ -eq $null -or $_ -eq '') {
    Throw "a is mandatory, please provide a value."
  }
  else {
    $True
  }
})]
Benjamin Hubbard
  • 2,797
  • 22
  • 28
  • Validators only get called on bound parameters (those explicitly supplied by a caller), so for a missing parameter they'll never be called. – briantist Nov 09 '15 at 04:47
  • Despite alx9r stating that his issue is with missing parameters, his example seems to be about getting PowerShell to recognize a parameter that is not actually missing. – Benjamin Hubbard Nov 09 '15 at 04:53
  • Yes that's true actually; his parameter is not missing. The problem is that the default value is still being applied (in a different context from when he would use he variable) and currently the default value was the only way he had of checking whether it was missing or not. But have you tried this out? `[ValidateScript()]` will not be called unless the parameter is already bound, so it can't be used to find a parameter that is not bound. – briantist Nov 09 '15 at 05:02
0

Start powershell in non-interactive mode:

powershell -NonInteractive
Casper Leon Nielsen
  • 2,528
  • 1
  • 28
  • 37
0

I have tried multiple of the above methods but all of them failed to do the simple requirement that a sensible message should be given to the user when the parameter is missing. Using ValidateScript Powershell would always add "Cannot validate argument on parameter 'fileName'" before my own messages, which I did not want.

I ended up going old school and just doing this:

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

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

This works. The only message I get is "File name is required".

gabnaim
  • 1,103
  • 9
  • 13