Parameter validation gotchas

I didn’t fail the test, I just found 100 ways to do it wrong.
— Benjamin Franklin

PowerShell’s parameter validation is a blessing. Validate parameters properly and you’ll never have to write a code that deals with erroneous user input. But sometimes dealing with Validation Attributes, requires a bit more knowledge that built-in help can provide. Here is what I’ve learned so far and want to share with you.

  • You can have more than one Validation Attribute

This may seem trivial, but PowerShell’s help and various online tutorials do not mention this fact (they just imply). You can have as much Validation Attributes as you like for your parameter. For example, this function requires parameter Number to be even and fall in range from 1 to 256:

function Test-MultipleValidationAttributes
{
    [CmdLetBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                if($_ % 2)
                {
                    throw 'Supply an even number!'
                }
                $true
        })]
        [ValidateRange(1,256)]
        [int]$Number
    )

    Process
    {
        Write-Host "Congratulations, $Number is an even and it's between 1 and 256!"
    }
}
  • You can have more than one instance of specific Validation Attribute

While not especially useful, it still could be used in some practical cases. Imagine, that you have some complex logic behind attribute validation with the ValidateScript attribute. Then it could be split between several ValidateScript attributes to decrease script complexity. Here is the the function from above, modified to validate parameters using two ValidateScript attributes. As a bonus, you can throw more meaningful error messages:

function Test-MultipleSimilarValidationAttributes
{
    [CmdLetBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                if($_ % 2)
                {
                    throw 'Supply an even number!'
                }
                $true
        })]
        [ValidateScript({
                if($_ -lt 1 -or $_ -gt 256)
                {
                    throw 'Supply number between 1 and 256!'
                }
                $true
        })]
        [int]$Number
    )

    Process
    {
        Write-Host "Congratulations, $Number is an even and it's between 1 and 256!"
    }
}
  • Once parameter is validated, it stays validated

Once you’ve set the rules, they apply to everyone, even to you. Want proof? Check this out:

function Test-PersistentValidation
{
    [CmdLetBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateRange(1,256)]
        [int]$Number
    )

    Process
    {
        $Number = 0
    }
}

Let’s see, where our rebellious spirit will lead us:

PS C:\Users\beatcracker> Test-PersistentValidation -Number 1
The variable cannot be validated because the value 0 is not a valid value for the Number variable.
At C:\Scripts\Test-Validation.ps1:13 char:9
+         $Number = 0
+         ~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ValidationMetadataException
    + FullyQualifiedErrorId : ValidateSetFailure

Not actually damnation of the soul, but close enough. This happens because PowerShell assigns Validation Attributes to the variable and they will stay there until variable is destroyed. You can view them, and I’ll show you how. Place a breakpoint on the line $Number = 0, call the function and wait for the debugger to pop up:

PS C:\Users\beatcracker> Test-PersistentValidation -Number 1
Hit Line breakpoint on 'C:\Scripts\Test-Validation.ps1:13'
[DBG]: PS C:\Scripts>> $tmp = Get-Variable Number

[DBG]: PS C:\Scripts>> $tmp.Attributes

TypeId
------
System.Management.Automation.ArgumentTypeConverterAttribute
System.Management.Automation.ValidateRangeAttribute
System.Management.Automation.ParameterAttribute

[DBG]: PS C:\Scripts>> $tmp.Attributes[1]

MinRange MaxRange TypeId
-------- -------- ------
       1      256 System.Management.Automation.ValidateRangeAttribute

And there is more to it! Starting with the PowerShell 3.0, you can place an attribute on any variable:

PS C:\Users\beatcracker> [ValidateRange(1,256)][int]$Number = 1

PS C:\Users\beatcracker> $Number = 0
The variable cannot be validated because the value 0 is not a valid value for the Number variable.
At line:1 char:1
+ $Number = 0
+ ~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [], ValidationMetadataException
    + FullyQualifiedErrorId : ValidateSetFailure

Details about this feature: New V3 Language Features, #PSTip Validate your custom objects.

  • Validation Attributes order matters

This the one of those things you learn the hard way. It’s not actually mentioned anywhere as far as I know. Below is a function, that accepts optional parameter Path, that has to be existing folder on disk. Since there is no sense in running validation script if supplied parameter is empty, there is a ValidateNotNullOrEmpty attribute added.

function Test-ValidationAttributesOrder
{
    [CmdLetBinding()]
    Param
    (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if(!(Test-Path -LiteralPath $_ -PathType Container))
            {
                throw "Folder not found: $_"
            }
            $true
        })]
        [string]$Path
    )

    Process
    {
        if($Path)
        {
            Write-Host "Optional parameter Path is a valid folder!"
        }
    }
}

Hence, when the function is called with the empty parameter, I expect it to throw “The argument is null or empty” error message. But instead, the validation fails where I do not expect it to:

PS C:\Users\beatcracker> Test-ValidationAttributesOrder -Path ''
Test-ValidationAttributesOrder : Cannot validate argument on parameter 'Path'. Cannot bind argument to parameter 'LiteralPath' because it is an empty string.
At line:1 char:38
+ Test-ValidationAttributesOrder -Path ''
+                                      ~~
    + CategoryInfo          : InvalidData: (:) [Test-ValidationAttributesOrder], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-ValidationAttributesOrder

As you see, my empty string passed to the Path parameter, completely missed ValidateNotNullOrEmpty attribute and landed directly at the validation script, where PowerShell rightly failed to bind it to LiteralPath parameter of the Test-Path cmdlet. Makes you wonder, doesn’t it? After some trial and error I was finally able to make sense of it:

PowerShell evaluates Validation Attributes in a Bottom to Top order

So, armed with this knowledge, let’s fix the function above and make it validate parameters in the correct order:

function Test-ValidationAttributesOrder
{
    [CmdLetBinding()]
    Param
    (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            if(!(Test-Path -LiteralPath $_ -PathType Container))
            {
                throw "Folder not found: $_"
            }
            $true
        })]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    Process
    {
        if($Path)
        {
            Write-Host "Optional parameter Path is a valid folder!"
        }
    }
}

And test it, passing the empty string to the Path parameter:

PS C:\Users\beatcracker> Test-ValidationAttributesOrder -Path ''
Test-ValidationAttributesOrder : Cannot validate argument on parameter 'Path'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At line:1 char:38
+ Test-ValidationAttributesOrder -Path ''
+                                      ~~
    + CategoryInfo          : InvalidData: (:) [Test-ValidationAttributesOrder], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-ValidationAttributesOrder

Voilà! Now it validates that parameter is not null or empty before passing parameter value to the validation script.

Advertisements

4 thoughts on “Parameter validation gotchas

    • Thanks, Sam. It’s something like a hobby of mine: to poke software with a debugger\disassembler and see what’s interesting will come out of it. I have other posts like that one, but unfortunately they’re in russian language. Here is Google-translated version in case you’re really interested.

      Like

      • Thanks for the link,Ill have a read through.In regards to nascar heat,ive had a bit of a dig through myself but unfortunately my knowledge of a debugger is limited. Ive found some interesting references in the code that no one has looked at before,would you be able to help poke them a little more?

        P.s you can download the free version (full game with no content for mod support) here https://www.heatfinder.net/downloads.php

        Like

        • I can try (though I too don’t possess Godlike assembly skills and can’t guarantee anything), but it has to wait till I’ll have some time on my hands. And since comments are not well suited for such discussion, it would be easier if you’d create a new thread in the Windows section of the Vogons.org board with the list of refrences you intersted in. Then you can post link to your thread here, or PM me there and I’ll see what I can do. This way, even if I wouldn’t be able to help you, maybe someone other from Vogons will.

          Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s