简体   繁体   中英

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.

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

演示

I like to use the Register-ArgumentCompleter cmdlet for that kind of things. If you are looking just for argument completion, then it will work perfectly. You'll have to do validation yourself within the function though as it won't prevent incorrect entry to be typed in.

It will however, provide a list of possible argument and the cities displayed will be only the cities associated to the State chosen.

Here's an example.

$Data = @'
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
'@| ConvertFrom-Csv -Delimiter '|'


Function Get-Population {
    Param(
        [Parameter(Mandatory = $true)]
        $State, 
        [Parameter(Mandatory = $true)]
        $City
    )
    return $Data | Where-Object { $_.State -eq $State -and $_.City -eq $City } | Select-Object -ExpandProperty Population 

} 

$PopulationArgCompletion = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    

    Filter CompleteWordExpand($Property) { if ($_.$Property -like "*$wordToComplete*") { return $_.$Property } }
    

    $ReturnData = $null

    switch ($ParameterName) {
        'State' { 
            $ReturnData = $Data | CompleteWordExpand -Property State  
        }
        'City' {
            if ($fakeBoundParameters.ContainsKey('State')) {
                $ReturnData = $Data |  Where-Object -Property State -eq $fakeBoundParameters.Item('State') | CompleteWordExpand -Property City 
            }
        }
        else {
            $ReturnData = $Data | CompleteWordExpand -Property City 
        }

    }

    if ($null -ne $ReturnData) {
        return $ReturnData | Select -Unique | ForEach-Object {
            $ctText = [System.Management.Automation.CompletionResultType]::Text
            $CompletionText = $_
            if ($_.indexof(" ") -gt -1) { $CompletionText = "'$_'" }
            [System.Management.Automation.CompletionResult]::new($CompletionText, $_, $ctText, $_)
        }
    }

}


Register-ArgumentCompleter -CommandName Get-Population -ParameterName State -ScriptBlock $PopulationArgCompletion
Register-ArgumentCompleter -CommandName Get-Population -ParameterName City -ScriptBlock $PopulationArgCompletion

Additional note

If you do test this, make sure to try it out in a different file than where you executed the script above. For some reason, VSCode and/or the PS extension do not show the argument completion if you try to do your testing (eg: Calling Get-Population to see the argument completion) in the same file you ran the script above.

Additional additional note

I edited my answer to include a CompleteWord filter that make it work properly with console tab completion, which was ommited from my initial answer. Since this is not really an in the code editor and I mostly never use the console directly, I had never took that into consideration.

Also, I added an additional check so that any state or cities that are multiple words get automatically surrounded by single quotes during the tab completion to avoid issues.

Bonus VSCode Snippet

Here is the snippet I use to generate quickly a template for the basis of doing argument completion everywhere when needed.

    "ArgumentCompletion": {
        "prefix": "ArgComplete",
        "body": [
            "$${1:MyArgumentCompletion} = {",
            "    param(\\$commandName, \\$parameterName, \\$wordToComplete, \\$commandAst, \\$fakeBoundParameters)",
            "",
            "      # [System.Management.Automation.CompletionResult]::new(\\$_)",
            "}",
            "",
            "Register-ArgumentCompleter -CommandName ${2:Command-Name} -ParameterName ${3:Name} -ScriptBlock $${1:MyArgumentCompletion}"
        ]
    }

References

Register-ArgumentCompleter

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM