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 ValidateSet parameter validation attribute to limit those values to only ones you define.

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.

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 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.
Cycling through parameter values using Tab Completion
Restricting values returned by Tab Completion

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.
Tab Completion and Intellisense in ISE

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!

Further Reading