Search code examples
powershelldynamicparameters

Generating dynamic validate set based on value of another parameter in PowerShell


A little background: We are working on a function that goes through hundreds of entries, similar to the following:

City State Population
New York New York 8467513
Los Angeles California 3849297
Chicago Illinois 2696555
Houston Texas 2288250
Phoenix Arizona 1624569
Philadelphia Pennsylvania 1576251
San Antonio Texas 1451853
San Diego California 1381611
Dallas Texas 1288457
San Jose California 983489

The raw data will be gotten using an Import-Csv. The CSV is updated on a regular basis.

We are trying to use PowerShell classes to enable people to select the City based on the State they select. Here is the MWE we have gotten so far:

$Global:StateData = Import-Csv \\path\to\city-state-population.csv

class State : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        return (($Global:StateData).State | Select-Object -Unique)
    }
}
class City : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues($State) {
        return ($Global:StateData | Where-Object State -eq $State).City
    }
}
function Get-Population {
    param (
        # Name of the state
        [Parameter(Mandatory, Position = 0)]
        [ValidateSet([State])]
        $State,

        # Name of the city
        [Parameter(Mandatory, Position = 1)]
        [ValidateSet([City])]
        $City
    )
    
    $City | ForEach-Object {
        $TargetCity = $City | Where-Object City -match $PSItem
        "The population of $($TargetCity.City), $($TargetCity.State) is $($TargetCity.Population)."
    }
}

Of course, according to the official documentation, GetValidValues() does not seem to accept parameter input. Is there a way to achieve this at all?

The results would need to be similar to PowerShell Function – Validating a Parameter Depending On A Previous Parameter’s Value, but the approach the post takes is beyond imagination for the amount of data we have.

Note: This is on PowerShell (Core), and not Windows PowerShell. The latter does not have the IValidateSetValuesGenerator interface.


Solution

  • I'm honestly not sure if you can do this with two ValidateSet Attribute Declarations, however, you could make it work with one ValidateSet and a custom class that implements the IArgumentCompleter Interface since it has access to $fakeBoundParameters. Here is an example, that for sure needs refinement but hopefully can get you on track.

    using namespace System.Management.Automation
    using namespace System.Management.Automation.Language
    using namespace System.Collections
    using namespace System.Collections.Generic
    
    class State : IValidateSetValuesGenerator {
        [string[]] GetValidValues() {
            return $script:StateData.State | Select-Object -Unique
        }
    }
    
    class Completer : IArgumentCompleter {
        [IEnumerable[CompletionResult]] CompleteArgument(
            [string] $CommandName,
            [string] $ParameterName,
            [string] $WordToComplete,
            [CommandAst] $CommandAst,
            [IDictionary] $FakeBoundParameters
        ) {
            [List[CompletionResult]] $result = foreach($line in $script:StateData) {
                if($line.State -ne $FakeBoundParameters['State'] -or $line.City -notlike "*$wordToComplete*") {
                    continue
                }
                $city = $line.City
                [CompletionResult]::new("'$city'", $city, [CompletionResultType]::ParameterValue, $city)
            }
            return $result
        }
    }
    
    function Get-Population {
        param(
            [Parameter(Mandatory, Position = 0)]
            [ValidateSet([State])]
            [string] $State,
    
            [Parameter(Mandatory, Position = 1)]
            [ArgumentCompleter([Completer])]
            [string] $City
        )
    
        "State: $State \\ City: $City"
    }
    

    Demo

    demo