PowerShell Parameter Validation
I've mentioned before that I'm an old-school IT Pro and scripter. I used to worry about my scripts failing halfway through because of bad data or values. When I run a script, or today a PowerShell function, I want to do everything I can to ensure that it runs successfully. This is especially true when I'm writing code that others might use. If there is going to be a problem, I want to identify it as soon as possible before the code can do anything.
PowerShell has many ways to validate parameter input, which you should take advantage of. You should never assume someone is going to run your function like you do. You can't assume they are going to enter a valid parameter value. You can't assume anything. You have no way of knowing where someone is getting their data from to pass to your function. Maybe they are defining a variable using some other code or importing from a file, and using that variable as a parameter value. You have no way of knowing what that data is going to be. The user may not even know!
This is also critical if your parameter accepts pipeline input. You have no way of knowing what is going to be passed to your function. You need to validate that data before you do anything with it. Again, if there is going to be a problem, you want to identify it as soon as possible. You don't want a command that deletes files to get halfway through and then fail because of a bad parameter value.
It doesn't take that much effort or time to add parameter validation to your code. Visual Studio Code makes it easy to add the validation attributes to your parameters.
I am not going to cover every possible validation option. I'll focus on the ones I think you will want to use often. I also want to provide some context and potential pitfalls.
Cast to Type
Before I dive into specific validation attributes, I want to encourage you to take advantage of another pseudo-validation option and that is casting the parameter value to a specific type. I know you've seen this before
Param(
[string]$Computername,
[int32]$Size,
[DateTime]$LimitDate,
[PSCredential]$Credential
)
This ensures that whatever value is passed to the parameter, PowerShell will attempt to convert it to the specified type. If it can't, PowerShell will throw an error and your code will fail to run. That is better than running with bad data and failing halfway through.
Sometimes this works to your advantage. If a user passes a string to the Size
parameter, but it looks like a number, PowerShell will try to convert it. You can test all of this in the console.
PS C:\> [int32]$Size = 10
PS C:\> $size
10
PS C:\> $size = "100"
PS C:\> $size
100
PS C:\> $size.GetType().Name
Int32
But you still need to be careful, depending on the type. This works as expected.
PS C:\> [DateTime]$LimitDate = "12/31/2024"
PS C:\> $limitDate
Tuesday, December 31, 2024 12:00:00 AM
If I try to use a string, PowerShell will complain.
PS C:\> $LimitDate = "foo"
MetadataError: Cannot convert value "foo" to type "System.DateTime". Error: "The string 'foo' was not recognized as a valid DateTime. There is an unknown word starting at index '0'."
Once you cast a parameter, or variable, to a specific type, PowerShell will enforce that type on all values, which may not always be what you expect.
PS C:\> $LimitDate = 1000
PS C:\> $LimitDate
Monday, January 1, 0001 12:00:00 AM
Casting your parameter variables to the correct type is a good start and definitely a best practice, but you might need additional validation.
ValidateNotNullOrEmpty
This is a simple validation attribute that I use on practically every parameter. This validation will ensure that the parameter value is not null or empty, something has been entered.
PS C:\> [ValidateNotNullOrEmpty()][string]$Item = "foo"
PS C:\> $item = "bar"
PS C:\> $item
bar
PS C:\> $item = $null
MetadataError: The variable cannot be validated because the value is not a valid value for the Item variable.
PS C:\> $item = ""
MetadataError: The variable cannot be validated because the value is not a valid value for the Item variable.
But be careful.
PS C:\> $item = " "
PS C:\> $item
PS C:\>
For a string, a space is not empty. You might need to add additional validation to ensure the string is not just whitespace. You can apply multiple parameter validation attributes to a parameter.
AllowNull
On the opposite side, there may be situations where having a Null parameter value is acceptable. Personally, I haven't found a use case for this attribute yet.
PS C:\> [AllowNull()][int]$x = 100
PS C:\> $x
100
PS C:\> $x = 200
PS C:\> $x
200
PS C:\> $x = $null
PS C:\> $x
0
PS C:\>
Again, the result might vary depending on the type. In this case PowerShell will treat null and empty strings as 0.
PS C:\> $x = ""
PS C:\> $x = " "
As long as your code takes this into account, this shouldn't be an issue. But this isn't always the case.
PS C:\> [AllowNull()][DateTime]$When = (Get-Date)
PS C:\> $when = $null
MetadataError: Cannot convert null to type "System.DateTime".
Even though you want to allow it, PowerShell can't convert null to a DateTime object. In this situation, using [AllowNull()]
isn't practical.
ValidateSet
Another validation attribute I use often is ValidateSet
. This attribute will ensure that the parameter value is one of the values you specify.
Function Get-It {
[CmdletBinding()]
Param(
[ValidateSet('One','Two','Three')]
[string]$Name = "One"
)
"Getting $Name"
}
This validation ensures the user will enter a pre-qualified value.
PS C:\> Get-It
Getting One
PS C:\> Get-It five
Get-It: Cannot validate argument on parameter 'Name'. The argument "five" does not belong to the set "One,Two,Three" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
Be careful with this validation, PowerShell will only assert it on bound parameter values. If you have a default parameter value, PowerShell will not validate it. Here's an example of what not to do.
Function Get-It {
[CmdletBinding()]
Param(
[ValidateSet('One','Two','Three')]
[string]$Name = "Four"
)
"Getting $Name"
}
PowerShell will happily run this function with the default value of "Four" even though it isn't in the set.
PS C:\> Get-It
Getting Four
Presumably, you are using [ValidateSet()]
to ensure that the user enters a valid value that will be used later in your code. The best practice is to ensure that any default value is also part of the validation set.
One benefit of using [ValidateSet()]
is that it provides auto completion in the console.
ValidateCount
It is not uncommon to define a parameter that accepts an array of values. Your code may require a minimum or maximum number of values. You can use [ValidateCount()]
to ensure that the user enters the correct number of values. This parameter validation requires parameters for the minimum and maximum number of values.
[ValidateCount(1,3)][string[]]$Names = @("X","Y","Z")
The user must enter at least one value and no more than three.
PS C:\> $names = "jeff"
PS C:\> $names = "a","b","c","d","e"
MetadataError: The variable cannot be validated because the value System.String[] is not a valid value for the Names variable.
If you want an exact number of values, set the minimum and maximum to the same value.
Function Get-Names {
[CmdletBinding()]
Param(
[Parameter(Mandatory)]
[ValidateCount(3,3)]
[string[]]$Names
)
$Names
}
This works fine when the user enters the correct number of values.
PS C:\> get-names foo,bar,banana
foo
bar
banana
But will fail as expected.
PS C:\> get-names foo
Get-Names: Cannot validate argument on parameter 'Names'. The parameter requires exactly 3 value(s) - 1 value(s) were provided.
By the way, if you notice in this example I made the parameter mandatory, PowerShell will continue prompting. It doesn't apply the validation until the user finishes.
PS C:\> Get-Names
cmdlet Get-Names at command pipeline position 1
Supply values for the following parameters:
Names[0]: foo
Names[1]: bar
Names[2]: this
Names[3]: that
Names[4]: banana
Names[5]: monkey
Names[6]:
Get-Names: Cannot validate argument on parameter 'Names'. The parameter requires exactly 3 value(s) - 6 value(s) were provided.
Be careful with this if the parameter is accepting pipeline input. Here's a revised version:
Function Get-Names {
[CmdletBinding()]
Param(
[Parameter(Mandatory,ValueFromPipeline)]
[ValidateCount(3,3)]
[string[]]$Names
)
Process {
$Names
}
}
This won't work the way you might expect.
PS C:\> "a","b","c" | Get-Names
Get-Names: Cannot validate argument on parameter 'Names'. The parameter requires exactly 3 value(s) - 1 value(s) were provided.
Get-Names: Cannot validate argument on parameter 'Names'. The parameter requires exactly 3 value(s) - 1 value(s) were provided.
Get-Names: Cannot validate argument on parameter 'Names'. The parameter requires exactly 3 value(s) - 1 value(s) were provided.
PowerShell is processing each piped object individually which will never pass parameter validation. This will work, but it requires the user to specify all values at once.
PS C:\> @(,@("a","b","c")) | Get-Names
a
b
c`
This example is piping the array @("a","b","c")
to the function as a single item which will pass parameter validation. You would want to document this behavior in your help text or reconsider how you want to structure your code.
Summary
I have a few more parameter validation attributes to cover, but I think this is enough for today. If you've not used these before, I encourage you to test them in your PowerShell console.