2

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.

Ram Iyer
  • 319
  • 1
  • 4
  • 14

2 Answers2

3

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

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Works exactly as intended in the question, in PowerShell (Core). Those using PowerShell 5.1 should note that the `IValidateSetValuesGenerator` interface isn't available on Windows PowerShell. – Ram Iyer Jul 01 '22 at 03:11
  • 1
    @RamIyer I just assumed you were using PowerShell Core because of the `IValidateSetValuesGenerator` implementation :) – Santiago Squarzon Jul 01 '22 at 03:52
  • 1
    I understand. This ran on my dev machine, but not on one of our servers, which is when I noticed this. I added the comment just to ensure those using legacy PowerShell are aware of it. On retrospect, that note should be on my question instead. Edited it. :) – Ram Iyer Jul 01 '22 at 05:43
3

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

Sage Pourpre
  • 9,932
  • 3
  • 27
  • 39
  • Works, thank you for this! Tab completion not working is a little caveat that might hinder the use of this in _my particular use case_, given the hundreds of possibilities of the "State" attribute of this example in my implementation. Although, not having to create a custom class makes it easier for non-.NET folks to implement this. – Ram Iyer Jun 28 '22 at 04:08
  • 1
    @RamIyer Tab completion do work though. The caveat is that if you work in vscode, have 1 file open (let's say "tmp.ps1"), paste my code and execute it, tab completion will work everywhere except within the "tmp.ps1" file. So you just need to make sure that the code that register the argument completer and where you want to use it is the same file. This is really an edge case and only a thing that might happen if you wanted to test it quickly but in any other situation, it would likely be imported from a file and / or module so it would never be an issue really. – Sage Pourpre Jun 28 '22 at 04:59
  • (It is really an oddball case but since I work a lot with argument completers and I have a tendancy to work from a single file during my testing phase, I noticed that behavior). – Sage Pourpre Jun 28 '22 at 05:00
  • Okay, noted, thank you! In fact, I'm struggling with implementing this across multiple cmdlets. If you could share the link to a sample of how you do it, it would be great. If this is not the place to ask for it, my apologies. – Ram Iyer Jun 29 '22 at 09:36
  • @RamIyer My answer demonstrate how I'd use it but you can see one implementation I used for my module SecretManagementArgumentCompleter [here](https://github.com/itfranck/SecretManagementArgumentCompleter/blob/aa1691833ac51e4e96a575b2f646bc7fae6bbed1/Output/SecretManagementArgumentCompleter/SecretManagementArgumentCompleter.psm1#L236). Also, since long chain or comments that deviate from the answer are discourage, here is a link to a [SO chat](https://chat.stackoverflow.com/rooms/246035/powershell-argument-completer) I created if you have questions – Sage Pourpre Jun 29 '22 at 17:25
  • @RamIyer Based on our chat interaction, I updated my answer to fix the issue you were describing. See the updated code along with the additional additional note. – Sage Pourpre Jul 01 '22 at 19:23