1

I'm trying to do something like this. The Get-SomeOtherParameter returns a system.Array type list from a database.

I don't want to hardcode my ValidateSet in case the list changes overTime in the database

function Get-SomeItems { 
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet(Get-SomeOtherParameter)] 
        [string]$filter,

        [Parameter(Mandatory = $true)]
        [ValidateSet('abc', 'def', 'ghi')] 
        [String]$filter2
    )
}
zett42
  • 25,437
  • 3
  • 35
  • 72

2 Answers2

3

To complement Start-Automating's helpful answer by spelling out the
[ValidateScript({ ... }] and [ArgumentCompleter({ ... }) approaches:

# Function that returns the valid values for the -filter parameter below.
function Get-ValidFilterValues { 
  # Sample, hard-coded values. This is where your database lookup would happen.
  'foo', 'bar'
}

function Get-SomeItems {
  param (
      [Parameter(Mandatory)]
      [ValidateScript({
        $validValues = Get-ValidFilterValues
        if ($_ -in $validValues) { return $true } # OK
        throw "'$_' is not a valid value. Use one of the following: '$($validValues -join ', ')'"
      })]
      [ArgumentCompleter({
        param($cmd, $param, $wordToComplete)
        (Get-ValidFilterValues) -like "$wordToComplete*" 
      })]      
      [string]$filter,

      [Parameter(Mandatory)]
      [ValidateSet('abc', 'def', 'ghi')] 
      [String]$filter2
  )

  $filter, $filter2 # sample output.

}

A simpler PowerShell (Core) 7+ alternative is to implement validation via a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface, which automatically also provides tab-completion:

# Custom class that implements the IValidateSetValuesGenerator interface 
# in order to return the valid values for the -filter parameter below.
class ValidFilterValues : System.Management.Automation.IValidateSetValuesGenerator { 
  [string[]] GetValidValues() { 
    # Sample, hard-coded values. This is where your database lookup would happen.
    return 'foo', 'bar'
  }
}

function Get-SomeItems {
  param (
      [Parameter(Mandatory)]
      [ValidateSet([ValidFilterValues])] # Pass the custom class defined above.
      [string]$filter,

      [Parameter(Mandatory)]
      [ValidateSet('abc', 'def', 'ghi')] 
      [String]$filter2
  )

  $filter, $filter2 # sample output.

}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Good point, @Start-Automating; please see my update (I've decided to spell out the `[ArgumentCompleter]` solution too, while I'm at it). – mklement0 Sep 22 '22 at 18:53
  • cool and all, except for it only being on 7. I didn't get too far into it for this answer, but I also build this whole transpiler for PowerShell, and can do scenarios like this without too much hassle in a way that works on multiple PowerShell versions. – Start-Automating Sep 22 '22 at 20:54
  • 1
    Already did one for build-time defined aliases: https://github.com/StartAutomating/PipeScript/blob/main/Transpilers/Parameters/Aliases.psx.ps1 – Start-Automating Sep 22 '22 at 21:01
2

There's two aspects to what you're trying to do:

  1. Making sure the parameter validation is correct
  2. Making the PowerShell experience around it "good" (aka supporting tab completion).

Parameter Validation :

As you might have already noticed [ValidateSet] is a hard-coded list. It's not really possible to soft code this (it is possible to dynamically build your script every time using some other modules, lemme know if you want more of an explainer for this).

To make the Validation work without [ValidateSet], I'd suggest [ValidateScript({})]. [ValidateScript] will run whatever script is in ValidateScript to ensure the script is valid. If the [ValidateScript()] throws, the user will see that message when they pass an invalid value in.

Tab-Completion :

To make it feel easy, you'll also want to add support for tab completion.

This is fairly straightforward using the [ArgumentCompleter] attribute.

Here's an example copied / pasted from a module called LightScript

[ArgumentCompleter({
        param ( $commandName,
            $parameterName,
            $wordToComplete,
            $commandAst,
            $fakeBoundParameters )
        $effectNames = @(Get-NanoLeaf -ListEffectName | 
            Select-Object -Unique) 
        if ($wordToComplete) {        
            $toComplete = $wordToComplete -replace "^'" -replace "'$"
            return @($effectNames -like "$toComplete*" -replace '^', "'" -replace '$',"'")
        } else {
            return @($effectNames -replace '^', "'" -replace '$',"'")
        }
    })]

This ArgumentCompleter does a few things:

  1. Calls some other command to get a list of effects
  2. If $wordToComplete was passed, finds all potential completions (while stripping off whitespace and enclosing in quotes)
  3. If $WordToComplete was not passed, puts each potential completion in quotes

Basically, all you should need to change are the command names / variables to make this work.

Hope this Helps

Start-Automating
  • 8,067
  • 2
  • 28
  • 47