Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
November 22, 2024

Dynamic Parameter Validation - Part 2

Last time I demonstrated a technique you can use in PowerShell 7 as an alternative to the static [ValidateSet()] parameter validation attribute. There is an alternative that comes close to the same functionality and it will also work in Windows PowerShell. You'll have to decide if it is appropriate for your situation.

Using an Enum

As I mentioned last time, [ValidateSet([class])] technique requires PowerShell 7. As an alternative for Windows PowerShell, you can use an enumeration. An enumeration is a programmatic way of defining a set of values. This is a common thing in PowerShell. The values for Write-Host colors are defined in a [ConsoleColor] enumeration.

PS C:\> [enum]::GetValues("ConsoleColor")
Black
DarkBlue
DarkGreen
DarkCyan
DarkRed
DarkMagenta
DarkYellow
Gray
DarkGray
Blue
Green
Cyan
Red
Magenta
Yellow
White

Here is the same example using an enumeration for the FileExtension class I demonstrated last time.

enum FileExtension {
    ps1
    ps1xml
    txt
    json
    xml
    yml
    zip
    md
    csv
}

You can enter this directly in your PowerShell session if you want to test. I can use my Get-TypeMember function from the PSScriptTools module to get the values.

PS C:\> Get-TypeMember FileExtension

   Type: FileExtension

Name     MemberType ResultType    IsStatic IsEnum
----     ---------- ----------    -------- ------
csv      Field      FileExtension     True
json     Field      FileExtension     True
md       Field      FileExtension     True
ps1      Field      FileExtension     True
ps1xml   Field      FileExtension     True
txt      Field      FileExtension     True
xml      Field      FileExtension     True
yml      Field      FileExtension     True
zip      Field      FileExtension     True
GetType  Method     Type
HasFlag  Method     Boolean
ToString Method     String

This approach using an enum will also work in PowerShell 7. In your function, set the parameter type to the enumeration.

[FileExtension]$Extension = 'ps1'

> Make sure any default values are defined in the enumeration.

Here's the revised function using the enumeration.

Function Measure-FileExtension {
    [CmdletBinding()]
    param(
        [Parameter(HelpMessage = 'Enter the path to analyze')]
        [string]$Path = '.',
        [ValidateNotNullOrEmpty()]
        [FileExtension]$Extension = 'ps1',
        [switch]$Recurse
    )

    $stats = Get-ChildItem -File -Path $Path -Filter "*.$Extension" -Recurse:$Recurse |
    Measure-Object -Property Length -Sum -Average -Maximum -Minimum

    [PSCustomObject]@{
        PSTypeName = 'FileExtensionStats'
        Path       = Convert-Path $Path
        Extension  = $Extension
        Files      = $stats.Count
        TotalSize  = $stats.Sum
        Average    = $stats.Average
    }
}

We won't use ValidateSet in this situation. The enumeration will provide the tab-completion and validation. If the user enters a value not defined in the enum, PowerShell will throw an error.

Enumeration failure
figure 1

The enum is a static list of values. If I want to change values, I have to edit the code and have the user reload everything.

A Dynamic Enum Option

That said, I have a hack that skirts this limitation. Instead of writing the enum in the script, I can create a C# enum definition and add it to the PowerShell session.

I'm going to revisit my Measure-FileExtension function. At the beginning of my script file which will eventually be dot-sourced, I'll use code like this:

# Step 1: Define your list of values
$list = Get-Content $PSScriptRoot\ExtensionList.txt
# Step 2: Create the Enum definition string
$enumDefinition = 'public enum FileExtension {'
foreach ($item in $list) {
    $enumDefinition += "$item,"
}
# Remove the trailing comma and close the enum definition
$enumDefinition = $enumDefinition.TrimEnd(',') + '}'

# Step 3: Use Add-Type to create the Enum

#You will get an error if you try to dot-source this again in the same session
#AND the enum has changed.
Try {
    Add-Type -TypeDefinition $enumDefinition -ErrorAction Stop
}
Catch {
    Write-Warning 'The [FileExtension] enum already exists and cannot be updated. Please re-run this script in a new PowerShell session.'
}

The code is creating a text block that defines the enumeration. The values are pulled from the text list. After Step 2, $enumDefinition will look like this:

public enum FileExtension {ps1,ps1xml,txt,json,xml,yml,zip,md,csv}

Step 3 adds the enumeration to the session using Add-Type. Due to the way .NET works, you'll get an error if you re-run the code with different values. .NET can't overwrite an existing enum. You'll need to start a new PowerShell session if there is a change.

The script file uses the enum as the type for the parameter.

Function Measure-FileExtension {
    [CmdletBinding()]
    Param(
        [Parameter(HelpMessage = 'Enter the path to analyze')]
        [string]$Path = '.',
        [ValidateNotNullOrEmpty()]
        [FileExtension]$Extension = 'ps1',
        [switch]$Recurse
    )

    $stats = Get-ChildItem -File -Path $Path -Filter "*.$Extension" -Recurse:$Recurse |
    Measure-Object -Property Length -Sum -Average -Maximum -Minimum

    [PSCustomObject]@{
        PSTypeName = 'FileExtensionStats'
        Path       = Convert-Path $Path
        Extension  = $Extension
        Files      = $stats.Count
        TotalSize  = $stats.Sum
        Average    = $stats.Average
    }
}

The end result is the same as using a static enum. This approach has a little more flexibility and I like that it separates the coded to define the enum from the enum data.

Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.