When you are writing a PowerShell script or function you often want to accept user input via parameters. If you don’t limit the values those parameters accept, you can guarantee that there will be situations where inappropriate values are supplied. In this article, learn how to use the PowerShell ValidateSet parameter validation attribute to limit those values to only ones you define.
Not a reader? Watch this related video tutorial!When writing a PowerShell script or function, you can use many different validation attributes to check that the values supplied to your parameters are acceptable and alert the user if they aren’t.
This article focuses on the ValidateSet validation attribute. You will learn what ValidateSet does, why you might want to use ValidateSet in your code and how to do so. You will also learn about the tab-completion feature that is enabled by ValidateSet and will help users of your code provide valid parameter values.
PowerShell ValidateSet: A Brief Overview
ValidateSet is a parameter attribute that allows you to define a set of elements that are only accepted as the value for that parameter.
For example, perhaps, you have a script that is defined to work with Active Directory domain controllers. This script has a parameter that specifies the name of a domain controller. Wouldn’t it make sense to limit the list of acceptable values to the actual names of the domain controllers? There’s no reason the user should be able to use “foobar” as a value when you know beforehand what values the script neds. ValidateSet gives you that capability.
Requirements
This article will be a learning walkthrough. If you plan to follow along, you will need the following:
- Visual Studio Code or any other code editor. I’ll be using Visual Studio Code.
- At least PowerShell 5.1 for the majority of the code in this article. There is one section that requires PowerShell 6.1 or later and I’ll identify that when we get to it
All of the code in this article has been tested in the following environments:
Operating System | PowerShell Versions |
---|---|
Windows 7 SP1 | 5.1, Core 6.2 |
Windows 10 1903 | 5.1, Core 6.2 |
Linux Mint 19.2 | Core 6.2 |
To help explain the concepts around ValidateSet you’re going to build a small script called Get-PlanetSize.ps1. This script returns information about the sizes of planets in our solar system.
You will start off with a simple script and gradually improve its ability to handle input from the end-user and make discovery of its possible parameter values easy for them.
Getting Started
To get started, first copy and paste the PowerShell code below into your favorite text editor and save it as Get-PlanetSize.ps1.
$planets = [ordered]@{
'Mercury' = 4879
'Venus' = 12104
'Earth' = 12756
'Mars' = 6805
'Jupiter' = 142984
'Saturn' = 120536
'Uranus' = 51118
'Neptune' = 49528
'Pluto' = 2306
}
$planets.keys | Foreach-Object {
$output = "The diameter of planet {0} is {1} km" -f $_, $planets[$_]
Write-Output $output
}
Run the script from a PowerShell prompt and you should get the following:
PS51> .\Get-PlanetSize.ps1
The diameter of planet Mercury is 4879 km
The diameter of planet Venus is 12104 km
The diameter of planet Earth is 12756 km
The diameter of planet Mars is 6805 km
The diameter of planet Jupiter is 142984 km
The diameter of planet Saturn is 120536 km
The diameter of planet Uranus is 51118 km
The diameter of planet Neptune is 49528 km
The diameter of planet Pluto is 2306 km
Informative but not very flexible; the information for every planet is returned, even if you only want the information for Mars.
Perhaps you’d like the capability to specify a single planet instead of returning all of them. You’d do that by introducing a parameter. Let’s look at how to achieve that next.
Accepting Input using a Parameter
To allow the script to accept a parameter, add a Param()
block to the top of the script. Call the parameter Planet
. A suitable Param()
block looks like below.
In the example below, the line [Parameter(Mandatory)]
ensures that a planet name is always supplied to the script. If it’s missing then the script will prompt for one.
Param(
[Parameter(Mandatory)]
$Planet
)
The simplest way to incorporate this Planet
parameter into the script is to change the line $planets.keys | Foreach-Object {
to $Planet | Foreach-Object {
. Now you’re not relying on the hashtable statically defined earlier and instead are reading the value of the Planet
parameter.
Now if you run the script and specify a planet using the Planet
parameter, you only see information about that particular planet.
PS51> .\Get-PlanetSize.ps1 -Planet Mars
The diameter of planet Mars is 6805 km
Excellent. Script completed? Maybe not.
The Options are Too Open
What happens if you try to find the diameter of the planet Barsoom using Get-PlanetSize.ps1 ?
PS51> .\Get-PlanetSize.ps1 -Planet Barsoom
The diameter of planet Barsoom is km
Hmm, that’s not right. Barsoom isn’t on the list of planets, but the script runs anyway. How can we fix this?
The problem here is that the script accepts any input and uses it, irrespective of whether it’s a valid value or not. The script needs a way to limit which values are accepted for the Planet
parameter. Enter ValidateSet!
Ensuring Only Certain Values are Used
A ValidateSet list is a comma-separated list of string values, wrapped in single or double-quotes. Adding a ValidateSet attribute to a script or function parameter consists of adding a line of text to the Param()
block, as shown below. Replace the Param()
block in your copy of Get-PlanetSize.ps1 with the one below and save the file.
Param(
[Parameter(Mandatory)]
[ValidateSet("Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto")]
$Planet
)
Try running the script again using Barsoom as your Planet
parameter. Now a helpful error message is returned. The message is specific in what went wrong and even provides a list of possible values for the parameter.
PS51> .\Get-PlanetSize.ps1 -Planet Barsoom
Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. The argument "Barsoom" does not belong to the set
"Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:32
.\Get-PlanetSize.ps1 -Planet Barsoom
~~~
CategoryInfo : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1
Making PowerShell ValidateSet Case Sensitive
By default, the ValidateSet attribute is case insensitive. This means that it will allow any string granted it’s in the allowed list with any capitalization scheme. For example, the above example will accept Mars
just as easily as it would accept mars
. If necessary, you can force ValidateSet to be case sensitive by using the IgnoreCase
option.
The IgnoreCase
option in ValidateSet a validation attribute determines whether the values supplied to the parameter match exactly to the valid values list. By default, IgnoreCase
is set to $True
(ignore case). If you set this to $False
then supplying mars as a value for the Planet
parameter for Get-PlanetSize.ps1
would generate an error message.
You’d use the IgnoreCase
option by assigning it a $true
value at the end of the list of valid values as shown below.
[ValidateSet("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", IgnoreCase = $false)]
Now when you attempt to use a value for Planet
that’s not exactly like a value in the list, validation will fail.
Using Tab Completion
Another perk of using ValidateSet is that it gives you tab completion. This means that you can cycle through the possible values for a parameter using the TAB key. This greatly improves the usability of a script or function, especially from the console.
In the examples below, there are a couple of things to note:
- The tab completion loops back to the first value after having displayed the last.
- The values are presented in alphabetical order, even though they are not listed in the ValidateSet alphabetically.
- Typing an initial letter and hitting TAB restricts the values offered by tab completion to those starting with that letter.
You can also take advantage of ValidateSet tab completion in the PowerShell Integrated Scripting Environment (ISE), as shown in the example below. The ISE Intellisense feature shows you the list of possible values in a nice selection box.
Intellisense returns all of the values containing the letter you type, rather than just those that start with it.
Now that we’ve covered ValidateSet validation attributes as they are in Windows 5.1, let’s take a look at what has been added in PowerShell Core 6.1 and see if that can give our script more validation capabilities.
Understanding Changes to ValidateSet in PowerShell 6.1
With the arrival of PowerShell Core 6.1 two new capabilities have been added to ValidateSet validation attributes:
- The
ErrorMessage
property - Use of classes in ValidateSet via access to
System.Management.Automation.IValidateSetValuesGenerator
The ErrorMessage Property
The default error message generated when you supply an incorrect planet name to Get-PlanetSize.ps1 is helpful, but a bit wordy:
PS61> .\Get-PlanetSize.ps1 -Planet Barsoom
Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. The argument "Barsoom" does not belong to the set
"Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
At line:1 char:32
.\Get-PlanetSize.ps1 -Planet Barsoom
~~~
CategoryInfo : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1
Use the ErrorMessage
property of the ValidateSet
validation attribute to set a different error message, as shown in the example below. {0}
is automatically replaced with the submitted value and {1}
is automatically replaced with the list of permitted values.
Param(
[Parameter(Mandatory)]
[ValidateSet("Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto",ErrorMessage="Value '{0}' is invalid. Try one of: '{1}'")]
$Planet
)
Replace the Param()
block in the script file and save it. Then try Get-PlanetSize.ps1 -Planet Barsoom
again. Notice below that the error is a lot less wordy and more descriptive.
PS61> .\Get-PlanetSize.ps1 -Planet Barsoom
Get-PlanetSize.ps1 : Cannot validate argument on parameter 'Planet'. Value 'Barsoom' is invalid. Try one of: 'Mercury,Venus,Earth,Mars,Jupiter,Saturn,Uranus,Neptune,Pluto'
At line:1 char:32
.\Get-PlanetSize.ps1 -Planet Barsoom
~~~
CategoryInfo : InvalidData: (:) [Get-PlanetSize.ps1], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationError,Get-PlanetSize.ps1
Next, take a look at a new way to define the acceptable values in ValidateSet via a PowerShell class.
PowerShell Classes
Custom types, known in PowerShell as classes, have been available since version 5. With the arrival of PowerShell Core 6.1 there is a new feature to allow the use of a class to provide the values for ValidateSet.
Using a class allows you to work around the main limitation of a ValidateSet – it is static. That is, it is embedded as part of the function or script, and can only be changed by editing the script itself.
The new feature that works with ValidateSet
is the ability to use the System.Management.Automation.IValidateSetValuesGenerator class. We can use this as a base for our own classes using inheritance. To work with ValidateSet
, the class must be based on System.Management.Automation.IValidateSetValuesGenerator and it must implement a method called GetValidValues()
.
The GetValues()
method returns the list of values that you wish to accept. A Param()
block with the static list of planets replaced by a [Planet]
class would look like below. This example won’t work yet. Keep reading to learn how to implement this.
Param(
[Parameter(Mandator)]
[ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
$Planet
)
Using a Class for a ValidateSet Value List: A Real Example
To demonstrate using a class for a ValidateSet value list, you’re going to replace the static list of planets used earlier with a list loaded from a CSV text file. You’re no longer going to need to maintain a static list of values inside of the script itself!
Creating a Data Source for the Class
First, you’ll need to create a CSV file containing each of the valid values. To do so, copy and paste this set of data into a new text file and save it as planets.csv in the same folder as the Get-PlanetSize.ps1 script.
Planet,Diameter
"Mercury","4879"
"Venus","12104"
"Earth","12756"
"Mars","6805"
"Jupiter","142984"
"Saturn","120536"
"Uranus","51118"
"Neptune","49528"
"Pluto","2306"
Adding the Class to the Script
Any class used by ValidateSet must already be defined before ValidateSet tries to use it. This means that the structure of Get-PlanetSize.ps1 as-is will not work.
The [Planet]
class has to be defined before it can be used, so it has to go at the start of the script. A suitable skeleton class definition looks like below:
class Planet : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() {
}
}
Inside the GetValidValues()
method of the class, use the Import-CSV
cmdlet to import the text file planets.csv created earlier. The file is imported into a variable with global scope, called $planets
, so to access it later in the script.
Use the return
statement to return the list of planet names via GetValidValues()
. After these changes the class should now look like below.
class Planet : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() {
$Global:planets = Import-CSV -Path planets.csv
return ($Global:planets).Planet
}
}
Next, remove the $planets
hash table declaration from the script as shown below. The global variable $planets
that gets populated by the [Planet]
class contains the planet data instead.
$planets = [ordered]@{
'Mercury' = 4879
'Venus' = 12104
'Earth' = 12756
'Mars' = 6805
'Jupiter' = 142984
'Saturn' = 120536
'Uranus' = 51118
'Neptune' = 49528
'Pluto' = 2306
}
Now wrap the remaining original code in a function and call it Get-PlanetDiameter
to differentiate it from the name of the script. Place the Param()
block that was at the start of the script inside the function. Replace the static list of planets with a reference to the [Planet]
class, as shown below.
[ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]`
Replace the line $output = "The diameter of planet {0} is {1} km" -f $_, $planets[$_]
with the following two lines. These allow the script to look up a planet in the array of objects created by Import-CSV
, rather than the hash table you created earlier, which you have removed from the script:
$targetplanet = $planets | Where -Property Planet -match $_
$output = "The diameter of planet {0} is {1} km" -f $targetplanet.Planet, $targetplanet.Diameter
After this set of changes your final script needs to look like this:
class Planet : System.Management.Automation.IValidateSetValuesGenerator {
[String[]] GetValidValues() {
$Global:planets = Import-CSV -Path planets.csv
return ($Global:planets).Planet
}
}
Function Get-PlanetDiameter {
Param(
[Parameter(Mandatory)]
[ValidateSet([Planet],ErrorMessage="Value '{0}' is invalid. Try one of: {1}")]
$Planet
)
$Planet | Foreach-Object {
$targetplanet = $planets | Where -Property Planet -match $_
$output = "The diameter of planet {0} is {1} km" -f $targetplanet.Planet, $targetplanet.Diameter
Write-Output $output
}
}
Remember, from this point on the script only works on PowerShell 6.1 or later
Now, how do you use this script?
Running the Script
You can’t use the new version of the script directly by simply executing the script. All of the useful code is now wrapped in a function. You need to dot source the file instead to allow access to the function from within the PowerShell session.
PS61> . .\Get-PlanetSize.ps1
Once dot-sourced, you now have access to the new Get-PlanetDiameter
function in the PowerShell session, with tab completion.
“What’s the benefit of all that work?”, I hear you ask. “The script seems to work in the same way, but it is more difficult to use the code!”
Try this:
- Open the
planets.csv
file that you created earlier. - Add a new row with a new name and diameter.
- Save the CSV file.
In the same session that you originally dot sourced your script, try looking up the diameter of the new planet using Get-PlanetDiameter
. It works!
Using a class in this way gives us several benefits:
- The list of valid values is now separate from the code itself, but any changes to the values in the file are picked up by the script.
- The file can be maintained by someone who never accesses the script.
- A more complex script could look up information from a spreadsheet, database, Active Directory or a web API.
As you can see, the possibilities are almost endless when using a class to provide ValidateSet values.
Wrapping up
We have covered a lot of ground as we’ve built Get-PlanetSize.ps1
, so let’s recap.
In this article you have learned:
- What a ValidateSet validation attribute is and why you might want to use one
- How to add ValidateSet to a PowerShell function or script
- How tab-completion works with ValidateSet
- How to use the
IgnoreCase
property to control whether your ValidateSet is case sensitive - How to use the
ErrorMessage
property with your ValidateSet and PowerShell 6.1 - How to use a class to make a dynamic ValidateSet with PowerShell 6.1
What are you waiting for? Start using ValidateSet today!