5
function Format-File {
  param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),

    [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [string] $key,

    [Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [string] $value
  )
}

I'm calling it like so, assume I've added values to the dictionary (removed for brevity here)

$dict = New-Object 'System.Collections.Generic.Dictionary[string,string]'
$dict.GetEnumerator() | Format-File -Path $updatePath

Here's my conundrum.

The above works perfectly. However, the following does not, note the difference in the key/value parameter

function Format-File {
  param(
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),

    [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [string] $key=$(throw "key is mandatory ($($MyInvocation.MyCommand))"),

    [Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [string] $value=$(throw "value is mandatory ($($MyInvocation.MyCommand))")
  )
}

The above throws an exception. It appears to be getting the default value when the function is first called, but when processing, the key/value parameters are set properly.

It makes a bit of sense as to why the key/value wouldn't be set at the time of the function call, but this also means my mental model is off.

So my question is two-fold.

  1. What is the parameter binding process for functions of this nature, and
  2. How does one verify the input of values that have come in from the pipeline? Manually check in the begin block, or is there another method?

If you have links to describe this all in greater detail, I'm happy to read up on it. It just made me realize my mental model of the process is flawed and I'm hoping to fix that.

Fred
  • 3,786
  • 7
  • 41
  • 52
  • Pipeline values are processed during `Process` that's the whole point. You don't see them before then because they may not exist when the function starts (pipeline's stream). – Etan Reisner May 01 '15 at 15:30

1 Answers1

6

What is the parameter binding process for functions of this nature?

In the Begin block, pipeline bound parameters will be $null or use their default value if there is one. This makes some sense, considering that the pipelining of values hasn't started yet.

In the Process block, the parameter will be the current item in the pipeline.

In the End block, the parameter will be the last value from the Process block, unless there was an exception in validating the parameter, in which case it will use the default (or $null).

How does one verify the input of values that have come in from the pipeline?

You can't check in the Begin block.

The best way is to use [Validate attributes, as you have with [ValidateNotNullOrEmpty()].

Your examples with using throw as a default value are useful in some situations but they are a clever workaround. The thing is, you don't need them since you already declared the parameter as Mandatory.

Instead of using a default value, you can use [ValidateScript( { $value -eq 'MyString' } )] for example.

Since the error message from [ValidateScript()] sucks, you can combine the techniques:

function Format-File {
  param(
    [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
    [ValidateNotNullOrEmpty()]
    [ValidateScript( {
        ($_.Length -le 10) -or $(throw "My custom exception message")
    } )]
    [string] $key
  )
}

Using [ValidateScript()] works whether it's a pipeline parameter or not.

briantist
  • 45,546
  • 6
  • 82
  • 127
  • 2
    Validation runs for each pipeline input? That's neat. And **dear lord** does the error output from `ValidateScript` suck. – Etan Reisner May 01 '15 at 15:33
  • 2
    Yes it runs on each pipeline input. And if you call it with `-ErrorAction Stop` (or its equivalent preference) it will stop when the exception is thrown, but if not, it will continue with the pipeline. – briantist May 01 '15 at 15:36
  • Does the ValidateScript attribute have any advantages over the default value trick in a normal function, or is the advantage more about binding at a more appropriate time when dealing with pipeline input? I'd like to continue using the default value trick in the general case as it's shorter, only using the validatescript when there's a clear case for it. – Fred May 01 '15 at 15:44
  • 1
    I can't think of a specific advantage, but before this question was posted I hadn't thought of its use in relation to pipelines either. But in my opinion this is the inherit advantage of doing it "the right way". I also think that it keeps (often messy) validation code out of the function body and makes it overall cleaner. I like the separation; I think parameter validation belongs in the declaration when possible. – briantist May 01 '15 at 15:46
  • I'll also say that the default value trick only works to validate that no value was specified. Why is that necessary when `Mandatory=$true` exists? – briantist May 01 '15 at 15:49
  • For me 2 reasons 1. Mandatory=true just causes PS to prompt the user instead of failing, and 2. in a lot of cases it's shorter to write so I find it easier to read. I'm sure you can adjust the behavior of 1, and I'm open to doing things differently, I just find the parameter lists to be nigh unreadable after a while and do everything I can to keep them shorter – Fred May 01 '15 at 15:52
  • this is where I got the trick from as it's describing the exact issue I was trying to work through a while back: http://stackoverflow.com/questions/9506056/is-it-possible-to-force-powershell-script-to-throw-if-a-required-parameter-is-om – Fred May 01 '15 at 15:53
  • I'm actually not sure you can change 1. (unless you can control the invocation of powershell to use `-NonInteractive`, that might work), so I understand that if you must not have PowerShell prompt. For 2, I'm a fan of verbose drawn out parameter declarations, so I have a lot of newlines in there to separate everything. ISE lets me collapse the whole param block anyway, so when I'm done I don't have to see it. I've definitely written 100-200 line parameter declarations. – briantist May 01 '15 at 15:55
  • and for normal usage 1 is ok, it's just I'm writing a more full blown 'system' in PS where it represents programmer error and it's more productive to have the immediate feedback get logged at the top. These functions aren't really designed for a human at a PS prompt to be using, so in that circumstance it makes sense for me to prefer throwing imo. – Fred May 01 '15 at 16:02
  • 1
    thanks for the knowledgeable response, my powershell knowledge has improved drastically today :) – Fred May 01 '15 at 16:03