PowerShell Parameter Validation Part 2
Let's continue exploring the world of parameter validation in your PowerShell scripts and functions. As I mentioned in the previous article, you can't make any assumptions about how a user is going pass values to your parameters. You can't assume they will always interactively enter a valid value. If a bad parameter value is going to cause a problem later in your code, you want to identify the problem as soon as you can and before any code is executed.
Today, I have a few more common validation tests to cover.
ValidateLength
Use this parameter validation when you want to ensure that parameter value is of a specific length. The value must be a string. You need to specify the minimum and maximum length of the string. Here is an example:
[ValidateLength(3,15)]$Computername = "SERVER23"
In this example, the value of $Computername
must be between 3 and 15 characters long. If the value is less than 3 or more than 15 characters, PowerShell will throw an error.
PS C:\> $computername="aa"
MetadataError: The variable cannot be validated because the value aa is not a valid value for the Computername variable.
If you want to specify a fixed length, you can use the same value for both the minimum and maximum length.
Function Test-String {
[CmdletBinding()]
Param(
[ValidateLength(10,10)]
[ValidateNotNullOrEmpty()]
[string]$Text
)
Write-Host "Testing $Text" -foreground yellow
}
In this example, the value of $Text
must be exactly 10 characters long.
PS C:\> Test-String abcdefghki
Testing abcdefghki
Any other length will throw an error:
PS C:\> Test-String abcdefghkis
Test-String: Cannot validate argument on parameter 'Text'. The character length of the 11 argument is too long. Shorten the character length of the argument so it is fewer than or equal to "10" characters, and then try the command again.
PS C:\> Test-String abcdefghk
Test-String: Cannot validate argument on parameter 'Text'. The character length (9) of the argument is too short. Specify an argument with a length that is greater than or equal to "10", and then try the command again.
If you want to test the length of an array, use [ValidateCount()]
which I covered last time.
ValidateRange
Sometimes, you want to ensure that a parameter value is within a specific range. This is not uncommon with integer values where you need to ensure the value falls within a given range. You can use the [ValidateRange()]
attribute for this purpose. Here is an example:
[ValidateRange(1,10)][int]$Days = 5
The value of $Days
must be between 1 and 10. Anything else will throw an error.
PS C:\> $days = 20
MetadataError: The variable cannot be validated because the value 20 is not a valid value for the Days variable.
PS C:\>
PS C:\> $days = 1
The range is inclusive, so the value of 1 is valid.
However, you aren't limited to just integers.
PS C:\> [ValidateRange(.1,1)][double]$Percent = .5
PS C:\> $percent
0.5
PS C:\> $percent = 2
MetadataError: The variable cannot be validated because the value 2 is not a valid value for the Percent variable.
PS C:\> $percent = .15
Just remember to use the appropriate data type for the range you are validating.
PS C:\> [ValidateRange("a","d")][char]$Character = "a"
PS C:\> $character="z"
MetadataError: The variable cannot be validated because the value z is not a valid value for the Character variable.
PS C:\> $character="aa"
MetadataError: Cannot convert value "aa" to type "System.Char". Error: "String must be exactly one character long."
PS C:\> $character="c"
ValidateRange Kind
In PowerShell 7, you have an additional option for this validation. You can specify the kind of comparison you want to use.
- Positive
- Negative
- NonNegative
- NonPositive
PS C:\> [ValidateRange("Positive")][int]$Count = 10
PS C:\> $count = 20
PS C:\> $count = -10
MetadataError: The variable cannot be validated because the value -10 is not a valid value for the Count variable.
This example demonstrates that the value of $Count
must be a positive integer. A value of 0 will fail Positive
and Negative
validations. This is where NonNegative
and NonPositive
come in.
PS C:\> [ValidateRange("NonNegative")][int]$Count = 5
PS C:\> [ValidateRange("NonNegative")][int]$Count = -5
MetadataError: The variable cannot be validated because the value -5 is not a valid value for the Count variable.
PS C:\> [ValidateRange("NonNegative")][int]$Count = 0
This option allows you to include 0 in the range.
ValidatePattern
Another common validation attribute for strings is [ValidatePattern()]
. This attribute allows you to specify a regular expression pattern that the value must match. Here is an example:
[ValidatePattern("^\d{3}-\d{3}-\d{4}$")][string]$Phone = "123-456-7890"
This ensures that the user enters the phone number in the correct format.
PS C:\> $phone = 1235550001
MetadataError: The variable cannot be validated because the value 1235550001 is not a valid value for the Phone variable.
PS C:\> $phone = "123-555-0001"
The pattern can be as simple or as complex as you need it to be. Here's a more succinct version of the same pattern.
[ValidatePattern("^(\d{3}-)+\d{4}$")][string]$Phone = "123-456-7890"
But don't feel you need to be a regex expert. There's no penalty for using the slightly longer pattern. When I build patterns, I use a site like Regex101.com to help me test and refine my patterns.
And while you can write negative matching regex patterns, I would recommend avoiding them if you can. Make your pattern a positive match for the value you want to accept.
ValidateScript
The last validation attribute I want to cover is one that you can use for any other validation you need. Create a script block with whatever code you need to test the parameter value. Use $_
in the script block to reference the parameter value. The code needs to write a result of $True
or $False
. The validation test should run very quickly.
[ValidateScript({(Get-Process).name -contains $_})][string]$ProcessName = "pwsh"
This validation ensures that the value of $ProcessName
is the name of a running process.
PS C:\> $processName = "code"
PS C:\> $processName = "foo"
MetadataError: The variable cannot be validated because the value foo is not a valid value for the ProcessName variable.
In PowerShell 7, you can take this a step further and define a custom error message.
Function Get-Data {
[CmdletBinding()]
Param(
[ValidateScript({(Get-Process).name -contains $_},
ErrorMessage = 'The value "{0}" is not a valid running process!')]
[string]$ProcessName = "pwsh"
)
Get-Process $ProcessName
}
It isn't required, but you can use {0}
as a placeholder for the value that failed the validation test.
> I recommend avoid using contractions or apostrophes in your error messages.
One thing to note, in this series of articles, I've been running most of the validation attributes directly at a PowerShell prompt. When you use the ErrorMessage with `[ValidateScript()]`, it only works in a script or function.
```powershell
PS C:\> Get-Data notepad
Get-Data: Cannot validate argument on parameter 'ProcessName'. The value "notepad" is not a valid running process!
PS C:\> notepad
PS C:\> Get-Data notepad
NPM(K) PM(M) WS(M) CPU(s) Id SI ProcessName
------ ----- ----- ------ -- -- -----------
39 78.55 89.55 0.95 27792 1 Notepad
13 3.10 15.45 0.06 39600 1 Notepad
If you are still running Windows PowerShell, you can still create a custom error message, but you need to take a slightly different approach. Remember, the validation attribute is expecting a result of $True
or $False
.
Function Get-Data {
[CmdletBinding()]
Param(
[ValidateScript({
if ((Get-Process).name -contains $_) {
$true
}
else {
Throw "The value '$_' is not a valid running process!"
}
})]
[string]$ProcessName = 'pwsh'
)
Get-Process $ProcessName
}
This command will work just as well in PowerShell 7.
You might want to use this approach instead of other validation attributes if you want more control over the error message.
Combining Validation Attributes
Finally, there's no reason you can't combine validation attributes. You can use as many as you need to ensure that the parameter value is correct.
Function Get-FolderData {
[cmdletbinding()]
Param(
[Parameter(Position=0, HelpMessage ="Specify a C:\ drive folder other than C:\Windows")]
[ValidateScript({Test-Path $_},ErrorMessage="The path {0} does not exist or cannot be verified.")]
[ValidateScript({ $_ -NotMatch 'c:\\windows' },ErrorMessage="The path {0} is not allowed.")]
[ValidatePattern("^c:\\")]
[ValidateCount(1, 3)]
[string[]]$Path = 'C:\Temp',
[Parameter(HelpMessage="Specify the number of days to use as a cutoff")]
[ValidateSet(7,14,30,90,180,365)]
[int]$Days = 30,
[Parameter(Mandatory,HelpMessage = "Specify the log file name.")]
[ValidateNotNullOrEmpty()]
[string]$LogFile
)
foreach ($item in $Path) {
Write-Host "Getting folder data for $item using the $Days days cutoff" -ForegroundColor Yellow
#code here
}
}
If you are using a script validation, it is possible to put everything in one script block. But in this example, I'm using an error message and if I combined everything, it would be more difficult to write a meaningful error message. This example, admittedly still contrived, demonstrates what is possible. Can you follow what each validation attribute is doing?
PS C:\> Get-FolderData -Path c:\temp -LogFile d:\temp\log.txt
Getting folder data for c:\temp using the 30 days cutoff
PS C:\> Get-FolderData -Path D:\Onedrive -LogFile d:\temp\log.txt
Get-FolderData: Cannot validate argument on parameter 'Path'. The argument "D:\Onedrive" does not match the "^c:\\" pattern. Supply an argument that matches "^c:\\" and try the command again.
PS C:\> Get-FolderData -Path c:\windows -LogFile d:\temp\log.txt
Get-FolderData: Cannot validate argument on parameter 'Path'. The path c:\windows is not allowed.
PS C:\> Get-FolderData -Path c:\temp,c:\work,c:\scripts,c:\reports -LogFile d:\temp\log.txt
Get-FolderData: Cannot validate argument on parameter 'Path'. The parameter requires at least 1 value(s) and no more than 3 value(s) - 4 value(s) were provided.
PS C:\> Get-FolderData -Path c:\temp,c:\foo -LogFile d:\temp\log.txt
Get-FolderData: Cannot validate argument on parameter 'Path'. The path c:\foo does not exist or cannot be verified.
PS C:\> Get-FolderData -Path c:\temp,c:\scripts -LogFile d:\temp\log.txt -days 20
Get-FolderData: Cannot validate argument on parameter 'Days'. The argument "20" does not belong to the set "7,14,30,90,180,365" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
PS C:\> Get-FolderData -Path c:\temp,c:\scripts -LogFile d:\temp\log.txt -days 90
Getting folder data for c:\temp using the 90 days cutoff
Getting folder data for c:\scripts using the 90 days cutoff
Be sure to test your validation attributes thoroughly, especially testing for values you know should fail. This is especially true when using [ValidatePattern()]
and [ValidateScript()]
.
Summary
Adding parameter validation does not take very long to add and can save you a lot of time and frustration later. I recommend including parameter requirements in your help documentation. Proper parameter validation ensures the user of your command will likely have a positive experience and that the command will have a high likelihood of success.
As always, please don't hesitate to leave comments or questions.