In my last article, I introduced the Practical PowerShell series. When working on PowerShell scripts, there might come a point where a set of instructions repeats code elsewhere in the script. It is also possible that you might want to incorporate code from elsewhere into your script so that you can easily call the code.
This description might remind you about cmdlets, which have names and, optionally, one or more parameters to control their operation. But what if you want the same thing for your code, like some code with a high reusability factor? Welcome to the world of PowerShell functions. Before we get too deep, let’s define exactly what we are talking about.
PowerShell makes it easy to pass values to a function in the form of parameters. However, it’s a good idea to validate parameters before executing any actions. Otherwise, unexpected values could cause a function to behave in a completely unpredictable way.
Before I get started, I want to point out that the techniques I’m about to demonstrate are only suitable for scenarios where you are passing a string value to a function and are certain that the string should contain one of several possible values. If you are working with other types of data or don’t know ahead of time which values for the string are acceptable, you must resort to using other validation techniques.
I have folder containing some folders, for the sake of argument, let’s say they are named MCD1.4
, MCD4.2
, MCD1.3rc
, tmp
and personal
.
I want to create a powershell script, say process.ps1
, inside my root folder that I can run “on” one of the listed folders. However, I only want to allow the script to run on folders with names beginning with MCD
. Also, for brevity, I want the input parameter to just be the version, without “MCD” in front. In other words, these should be allowed:
.\process.ps1 -version 4.2
.\process.ps1 -version 1.4
.\process.ps1 -version 1.3rc
param(
[Parameter(Position=0)]
[ValidateSet('4.2','1.4','1.3rc')
[System.String]$version
)
Write-Host "Now processing folder MCD$version"
And this script behaves exactly as I want it to, as long as the folder structure remains the same.
However, I want to make this more dynamic, with the script taking into account the actual folder structure. What I wanted to do is this:
$folders = Get-ChildItem . -Directory
$options = [System.Collections.ArrayList]::new()
foreach ($item in $folders) {
if ($item.Name.StartsWith("MCD")) {
$options.Add($item.Name.Substring(3))
}
}
param(
[Parameter(Position=0)]
[ValidateSet($options)
[System.String]$version
)
Write-Host "Now processing folder MCD$version"
but this does not work because apparently, the param
argument needs to be listed at the very top of the ps1
file. Is there any way I can achieve the desired behavior of my script? The script above throws this error:
At C:\path\to\process.ps1:15 char:18
+ [ValidateSet($options)]
+ ~~~~~~~~
Attribute argument must be a constant or a script block.
+ CategoryInfo : ParserError: (:) [], ParseException
+ FullyQualifiedErrorId : ParameterAttributeArgumentNeedsToBeConstantOrScriptBlock
Using parameters for your Scripts and Functions is very powerful. You don’t have to hardcode things in them, making running them from a command line easier. This blog post will show you the parameters I use in most of my scripts and how they work.
HelpMessage

You can see that the scripts prompted for a value for the -Filename parameter, and it also tells you to use !? For Help. Typing !? Shows you the HelpMessage that you configured in the parameter, in this case it tells you to use a complete path to the filename with an example of c:\temp\powershellisfun.txt.
Mandatory
If you need to use a parameter, specify it using Mandatory = $true. If not, then you can use Mandatory = $false. For example:
param( [parameter(Mandatory = $true)][string[]]$ComputerName = $env:COMPUTERNAME )
In the example above, the -ComputerName parameter must be used. However, if no value is specified, it will automatically use $env:COMPUTERNAME as the value. If you don’t use the -ComputerName parameter and the Mandatory option is set to $true, it will automatically use $env:COMPUTERNAME.
Running this example will look like this:

ParameterSetname
If you have multiple parameters and a few that can’t be used together, you can use the ParameterSetName option. In the example script below, I have the -PowerShellEventlogOnly and -PSReadLineHistoryOnly parameters that can’t be used together. If you run the script and use the – key with Tab to show the parameters, they will show both. If you select one, try to choose the other. Then, you will see that it’s no longer available for usage.

Switch

You can see that running the switch.ps1 script with the -filename parameter will display the contents of the powershellisfun.txt on your screen. The -GridView parameter with a $false value will also be outputted on your screen. The -GridView parameter, with a default value of $true, will output the contents in an Out-Gridview pane.
ValidateSet
Using the ValidateSet option, you can specify a parameter with one of the configured values. This is easier than prompting or supplying values that might have typos. In the example below, I used a set of values for the state of a Windows Service that can be passed as a parameter.

You can see that when using the -StartupType parameter, the available values shown are the ones configured in the ValidateSet values. When I tried to type a value not in the list, valuenotinlist, in this case for testing, it threw an error that it’s not in the ValidateSet values. “The argument “valuenotinlist” does not belong to the set “Automatic,Boot,Disabled,Manual,System” specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.”
One of the coolest yet complex features of advanced functions in PowerShell is dynamic parameters and using that feature to perform PowerShell parameter validation. Dynamic parameters take your typical function parameters to a whole new level.
Have you ever had a time when you created an advanced function and wanted your parameters to depend on something else; to dynamically be created based on the criteria you choose at runtime?
How about wanting Powershell parameter validation like building a PowerShell ValidateSet
array providing tab-completion on a parameter not based on a static set of stings but generated at runtime? These are both doable with dynamic parameters.
In this series, we also have:
- Standard and Advanced PowerShell Functions by Francois-Xavier Cat (@LazyWinAdm) (March 30, 2015)
- PowerShell Advanced Functions: Can we build them better? With parameter validation, yes we can!byMike F. Robbins (@mikefrobbins) (March 31, 2015)
- Supporting WhatIf and Confirm in Advanced Functions by Jeffery Hicks (@JeffHicks) (April 2, 2015)
- Advanced Help for Advanced Functions by June Blender (@juneb_get_help) (April 3, 2015)
- A Look at Try/Catch in PowerShell by Boe Prox (@proxb) (April 4, 2015)
To suggest a PowerShell Blogging Week topic, leave a comment or tweet it to us with the #PSBlogWeek hashtag.
There are a couple of different ways to use dynamic parameters that I’ve seen. The first is the way that Ben Ten wrote about them on PowerShell Magazine. Using this method, Ben was able to create parameters on the fly based on if a different parameter was used. Personally, I’ve never had a need to do this.
I really like using dynamic parameters as a way to validate input based on some criteria that are available at runtime. This way I can write a script that gathers information on-the-fly which allows me the beautiful parameter tab-completion we all know and love.
Let’s go over an example on how to create Powershell parameter validation based on files in a folder.

You’ll notice in the example above I’m using the Get-Item
cmdlet and the default parameters for tab-completion which is to be expected. I want that functionality but I want to tab-complete my own arguments so let’s create a simple function to do that.

You’ll notice that I’ve highlighted the validation attribute that will allow us to tab-complete the MyParameter
argument. Now we’re able to get custom parameter argument tab-completion using the values specified in the PowerShell ValidateSet
array attribute.

But now what if I want my tab-completion options to be generated on-the-fly based on some other criteria rather than a static list? The only option is to use dynamic parameters. In my example, I want to tab-complete a list of files in a particular folder at run-time.
To get this done I’ll be using a dynamic parameter which will run Get-ChildItem
whenever I try to tab-complete the MyParameter
parameter.
With that being said, let’s make the ValidateSet
attribute of the MyParameter
parameter dynamic, shall we?
[CmdletBinding()]
param()
DynamicParam {
}
Make PowerShell Work for You
It is ambitious to discuss functions and parameters in PowerShell in one article. I have not touched on other subjects, such as command sets and dynamic parameters. These might be topics for another article.
I hope that I have encouraged you to write reusable code, not only to leverage scripts and functions but also to correctly define code. Make PowerShell work for you when possible, letting it handle parameter validation and checking other constraints such as types. This enables you to focus on the task while making code less complex and improving readability.
If you have questions or comments, feel free to reach out in the comments. If not, wait until the next article, where I will discuss flow control.
Creating a Dynamic ValidateSet Array Parameter the Easy Way
New-ValidationDynamicParam -Name 'MyParameter' -Mandatory -ValidateSetOptions (Get-ChildItem C:\TheAwesome -File | Select-Object -ExpandProperty Name)
My pain is your gain, people!
Now, with our dynamic validation parameter created, let’s take it for test drive.
I’ve got some files in a directory on my computer that I only want to be passed to the MyParameter
parameter.

Now all I have to do is run our script and voila! I’m now only able to use the file names as parameter arguments and they are updated as the files comes in and out of the folder!

Scripts
A script is a text file with a .ps1 extension containing PowerShell code. The code consists of cmdlets and (optionally) functions. You can call scripts in various ways:
- Using the ampersand (& .\Process-Something.ps1). The code runs in a child session with its scope. This means any definitions, such as variables or functions in the script, disappear when the script terminates. When you run interactive PowerShell scripts, you usually omit the ampersand, but when you want to run code pointed to in a variable, you must use this invocation method, e.g., & { Get-ChildItem }
Make sure to understand the difference between using the ampersand at the start and end of a command. Using an ampersand at the end instructs PowerShell to run the code in a background job.
- Using dot-sourcing (e.g. .\Helper-Functions.ps1)., the code runs in the current PowerShell session. This means that any variables or functions you define in the script become available in the session. If these definitions existed before, they would simply be overwritten.
The ability to run PowerShell scripts depends on the local machine’s current execution policy. This is a security measure to prevent malicious scripts from running. Scripts from Microsoft are generally signed, but many community-sourced scripts downloaded from the internet are usually not. You may need to run Set-ExecutionPolicy unrestricted before you can run scripts created by others, provided company policies do not prevent you from modifying the execution policy.
#2. Validation Check Using ValidateSet
Now that I have demonstrated how to perform parameter validation by using a -Contains operator, I will introduce a different technique that accomplishes more or less the same thing.
Here is the script:
There is a way to handle errors more gracefully, but requires you to be running PowerShell 7 or higher.
PowerShell 7 greatly improves error handling.
Reusable Code
Function Get-MyReport { #reusable code }
If you create a function that redefines an existing command, the existing definition is overwritten in the current session. The code’s output will be returned to the caller, and you can output the result to the screen or assign it to a variable for further processing.
In addition to the function itself, you can also define parameters for the code contained in the function. The code within the function can then perform its task using information passed through the parameters, as the parameters become variables usable in the context of the function’s code.
Function Get-DistributionGroupInfo( $Identity, $MemberCount) { Get-DistributionGroup $Identity | Select-Object Identity,PrimarySmtpAddress, @{n='MemberCount';e={ If( $MemberCount) { (Get-DistributionGroupMember -Identity $_ | Measure-Object).Count } }}} }
This code is acceptable when drafting a script or working on a proof of concept. However, issues might become apparent when using the function later or somebody else who is less familiar with the use case takes responsibility for the code.
Among the issues with this function are:
- The distribution group passed in the variable $Identity can be unspecified ($null). This can lead to unintended side effects, as many Get cmdlets happily return all objects when you specify $null as a value. Take the following code:
Function Process-Mailbox( $Id) { Get-Mailbox -Identity $Id | Set-Mailbox -HiddenFromAddressListEnabled $True }
Can you guess what happens if you do not pass the ID parameter or when the ID is empty? All mailboxes are returned and Set-Mailbox will happily hide all the mailboxes from address lists. It is probably not something you intended.
- In the example above, Identity and Members can be anything; they do not need to be a distribution group or switch, respectively. You might add code that checks if $Identity is a distribution group and if Members is a Boolean ($true or $false), but that would require additional code. The code might become quite complex if it must process multiple parameters.
Function Get-DistributionGroupInfo { [CmdletBinding()] Param( [Parameter(Position= 0, Mandatory= $true, ValueFromPipeline= $true, ValueFromPipelineByPropertyName=$true, HelpMessage= 'Please provide a Distribution Group')] [String]$Identity, [Parameter(HelpMessage= 'Output member count')] [Switch]$MemberCount ) Process { Write-Verbose ('Fetching Distribution Group {0}' -f $Identity) Get-DistributionGroup $Identity | Select-Object Identity,PrimarySmtpAddress, @{n='MemberCount';e={ If( $MemberCount) { (Get-DistributionGroupMember -Identity $_ | Measure-Object).Count } }} } }
- [CmdletBinding()] before the first parameter definition tells PowerShell that the function supports common parameters. Examples of common parameters are Verbose and Confirm. You can then include code in your function to support this. For example, if you pass -Verbose and your function contains Write-Verbose commands, verbose output will be displayed. When you omit -Verbose, the output is not displayed.
- Position=0 in the first parameter specification instructs PowerShell that the first unnamed parameter passed to Get-DistributionGroupInfo is treated as the Identity. So, the following two commands are the same:
Get-DistributionGroupInfo -Identity 'DG-X'
Get-DistributionGroupInfo 'DG-X'
Additional unnamed parameters can be specified as Position=1, etc.
- Mandatory=$true tells PowerShell that this parameter is mandatory. When a user omits the Identity when running the script, PowerShell will ask for it. Parameters can be optional by setting Mandatory to $false or omitting the condition.
- We want to be able to pass the Identity when the function is called in a pipeline. You can enable pipeline usage for this parameter by specifying ValueFromPipeline. You can use the property of passed objects by specifying ValueFromPipelineByPropertyName. Calling the function in a pipeline can then look like this:
Get-DistributionGroup -Identity 'DG-X' | Get-DistributionGroupInfo
- When you omit a mandatory parameter, HelpMessage defines help information. This help information is displayed when you enter !? when PowerShell asks you for input, and it is also displayed when you use:
Get-Help Get-DistributionGroupInfo -Full
I do not know anyone who uses !?, but you can if you want.
- After specifying the constraints, you define the parameter itself. You do this by giving it a name or, in this case, an identity. This name contains the value passed as a parameter when the function is called, making it available within its scope. You can optionally define the type of object the parameter will accept. In this case, we specify [String], which equals [System.String], but PowerShell has some type accelerators (short aliases) for built-in types.
Function Test { param( [int]$A ) Write-Output ($A) } ❯ test -A 123 123 ❯ test -A 'string' Test: Cannot process argument transformation on parameter 'A'. Cannot convert value "string" to type "System.Int32". Error: "The input string 'string' was not in a correct format."
I recommend using strict typing with parameters whenever possible. Typing helps with troubleshooting usage and also helps to document the code, as explained later. One thing to consider is that values might get converted through interpretation. For example, if you pass a parameter value 123, and a string is expected, PowerShell will happily convert this to a string representation, ‘123’.
- The second parameter (note the comma after Identity) is a Switch named MemberCount. Since this parameter is not mandatory and pipeline usage is unnecessary, these are not specified in the definition. The nice thing with switches is that you can use them just by mentioning, e.g. –MemberCount ; will set the MemberCount variable to $true. When you do not, it will be $false. You cannot use -MemberCount $true, as PowerShell will interpret $true as the next parameter since MemberCount is a switch. If needed, for example when $true or $false is stored in a variable, you can set it using a variable by specifying <Switch>:<value>, e.g. -MemberCount:$false. You might already be using this syntax when avoiding having to confirm certain commands, e.g. Set-Mailbox … -Confirm:$False
- By putting the code performing the actual task in a Process script block {}, we make the function work for objects passed through the pipeline. If we omit this and leave the code as-is, it will not support pipelining, and the code will only execute once for the last object received through the pipeline. Note that the current object in the pipeline is available through the automatic variable $_, if needed, within the Process script block.
When we put the code for this function in a script file, for example, MyDemo.ps1, it becomes available within our PowerShell session. To accomplish this, we need to dot source it to define it in our session. We can then call it – provided the Exchange Online Management Shell is loaded and connected – and inspect its definition by calling Get-Help, which also includes documentation.
PS❯ . .\MyDemo.ps1 PS❯ Get-DistributionGroupInfo -Identity MyDG -MemberCount -Verbose VERBOSE: Fetching Distribution Group MyDG Identity PrimarySmtpAddress MemberCount -------- ------------------ ----------- MyDG MyDG@contoso.com 2 PS❯ Get-DistributionGroup | Get-DistributionGroupInfo -MemberCount Identity PrimarySmtpAddress MemberCount -------- ------------------ ----------- MyDG MyDG@contoso.com 2 OtherDG OtherDG@contoso.com 8 PS❯ Get-Help Get-DistributionGroupInfo -Full NAME Get-DistributionGroupInfo SYNTAX Get-DistributionGroupInfo [-Identity] <string> [-MemberCount] [<CommonParameters>] PARAMETERS -Identity <string> Please provide a Distribution Group. Required? True Position? 0 Accept pipeline input? true (ByValue) Parameter set name (All) Aliases None Dynamic? False Accept wildcard characters? False -MemberCount Output member count Required? False Position? Named Accept pipeline input? False Parameter set name (All) Aliases None Dynamic? False Accept wildcard characters? False <CommonParameters> This cmdlet supports the common parameters: Verbose, Debug, ErrorAction, ErrorVariable, WarningAction, WarningVariable, OutBuffer, PipelineVariable, and OutVariable. For more information, see about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216).
#1. Validation Check Using the -Contains Operator
So, with that said, here is a very simple PowerShell script that demonstrates the first validation technique:
Looking at the script, you’ll notice that I created a variable called $Colors and have set it to be equal to “Red”, “Green”, “Blue”, “Yellow”, “Gray”. These are the colors that the script will support. While PowerShell recognizes a broader range of color names, the script deliberately focuses on these five colors to keep things simple.
This is what my script does.
Creating a Dynamic Powershell Parameter Validation the Hard Way
A dynamic parameter is, in a sense, a System.Management.Automation.RuntimeDefinedParameterDictionary
object with one or more System.Management.Automation.RuntimeDefinedParameter
objects inside of it. But it’s not quite that easy. Let’s break it down.
- First, instantiate a new
System.Management.Automation.RuntimeDefinedParameterDictionary
object to use as a container for the one or more parameters we’ll be adding to it using
$RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
.
2. Next, create the System.Collections.ObjectModel.Collection
prepped to contain System.Attribute
objects.
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
.
3. Now instantiate a System.Management.Automation.ParameterAttribute
object which will hold all of the parameter attributes we’re used to. In our instance, I’m defining my parameter to be in all the parameter sets and accept pipeline input by a pipeline object and by property name.
$ParamAttrib = New-Object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $Mandatory.IsPresent
$ParamAttrib.ParameterSetName = '__AllParameterSets'
$ParamAttrib.ValueFromPipeline = $ValueFromPipeline.IsPresent
$ParamAttrib.ValueFromPipelineByPropertyName = $ValueFromPipelineByPropertyName.IsPresent
4. Add our parameter attribute set to the collection we instantiated above.
$AttribColl.Add($ParamAttrib)
5. Because I’m using this dynamic parameter to build a PowerShell ValidateSet array for parameter validation I must also include a System.Management.Automation.ValidateSetAttribute
object inside of our attribute collection. This is where you define the code to actually create the values that allows us to tab-complete the parameter arguments.
$AttribColl.Add((New-Object System.Management.Automation.ValidateSetAttribute((Get-ChildItem C:\TheAwesome -File | Select-Object -ExpandProperty Name))))
6. We then have to instantiate a System.Management.Automation.RuntimeDefinedParameter
object using the parameter name, it’s type and the attribute collection we’ve been adding stuff to.
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter('MyParameter', [string], $AttribColl)
7. Once the run time parameter is finished we then come back to that original dictionary object we instantiated earlier using the parameter name and the runtime parameter object we created.
$RuntimeParamDic.Add('MyParameter', $RuntimeParam)
Are your eyes glazing over yet? Mine was when I first tried to figure this out.
Script Parameters
[CmdletBinding()] Param( [parameter( Mandatory= $true) [ValidateScript({ Test-Path -Path $_ -PathType Leaf})] [String]$CSVFile, [ValidateSet(',', ';')] [string]$Delimiter=',', [System.Security.SecureString]$Password ) Function X { # … } #etc.
- Ask to provide a value for $CSVFile when one has not been given (Mandatory= $true). You will notice a ValidateScript line as part of the CSVFile parameter definition. When specifying parameters, you can have PowerShell perform certain validations against the values provided. Some of the possible tests are:
- ValidateScript is used to execute a script block that needs to result in $true for the parameter value to be accepted. In the example, we check if the filename is valid using Test-Path specifying the automatic variable $_ (the actual filename).
- ValidateSet to test the value against a set of predefined values. In the example, we use ValidateSet only to allow a comma or semi-colon for the $Delimiter parameter.
- The delimiter parameter can be specified. If it is not specified (not mandatory), we set it to a default value of ‘,.’
- A $Password parameter can be provided. When specified, it needs to be of the type [System.Security.SecureString]. You are not limited to PowerShell’s built-in types; you can also use other (.NET) types, such as SecureString or a credential of the type [PSCredential].
Wrapping up
In this blog post, I showed you some examples of parameters that you can use in your scripts to make things more dynamic and easier. Hardcoding variables in scripts is not something you should do for scripts that could be used by more people other than you 😉
Begin, Process, End
Begin { # Initialize $Items=0 } Process { # Do Something $Items++ } End { # Cleanup Write-Host ('We processed {0} object(s)' -f $Items) }
An example is when you want to count how many objects you have processed, as the number of objects passed through a pipeline is unknown upfront. Another example of this would be the Sort-Object command, which can only sort objects when all objects have been passed to it.
About the Author(s)
Brien Posey is a bestselling technology author, a speaker, and a 20X Microsoft MVP. In addition to his ongoing work in IT, Posey has spent the last several years training as a commercial astronaut candidate in preparation to fly on a mission to study polar mesospheric clouds from space.