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
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.