Jason Thompson
Jason Thompson

Reputation: 4833

What is the proper way to define a dynamic ValidateSet in a PowerShell script?

I have a PowerShell 7.1 helper script that I use to copy projects from subversion to my local device. I'd like to make this script easier for me to use by enabling PowerShell to auto-complete parameters into this script. After some research, it looks like I can implement an interface to provide valid parameters via a ValidateSet.

Based on Microsoft's documentation, I attempted to do this like so:

[CmdletBinding()]
param (
    [Parameter(Mandatory)]
    [ValidateSet([ProjectNames])]
    [String]
    $ProjectName,

    #Other params
)

Class ProjectNames : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        # logic to return projects here.
    }
}

When I run this, it does not auto-complete and I get the following error:

❯ Copy-ProjectFromSubversion.ps1 my-project
InvalidOperation: C:\OneDrive\Powershell-Scripts\Copy-ProjectFromSubversion.ps1:4
Line |
   4 |      [ValidateSet([ProjectNames])]
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Unable to find type [ProjectNames].

This makes sense since the class isn't defined until after the parameters. So I moved the class above the parameters. Obviously this is a syntax error. So how do I do this? Is it not possible in a simple PowerShell script?

Upvotes: 11

Views: 5844

Answers (2)

Vasil Lilov
Vasil Lilov

Reputation: 1

Answer is correct, but Error Message is introduced in Powershell 6. Also, the reason, I assume, you are getting is because you are using earlier version as well. Powershell 5.1 does not have IValidateSetValuesGenerator (interface).

If you are using 5.1 use the code suggested beside the ErrorMessage. Beside that great example!

Upvotes: 0

mklement0
mklement0

Reputation: 437998

Indeed, you've hit a catch-22: for the parameter declaration to work during the script-parsing phase, class [ProjectNames] must already be defined, yet you're not allowed to place the class definition before the parameter declaration.

The closest approximation of your intent using a stand-alone script file (.ps1) is to use the ValidateScript attribute instead:

[CmdletBinding()]
param (
  [Parameter(Mandatory)]
  [ValidateScript(
    { $_ -in (Get-ChildItem -Directory).Name },
    ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
  )]
  [String] $ProjectName # ...
)

Limitations:

  • [ValidateScript] does not and cannot provide tab-completion: the script block, { ... }, providing the validation is only expected to return a Boolean, and there's no guarantee that a discrete set of values is even involved.

  • Similarly, you can't reference the dynamically generated set of valid values (as generated inside the script block) in the ErrorMessage property value.

The only way around these limitations would be to duplicate that part of the script block that calculates the valid values, but that can become a maintenance headache.

To get tab-completion you'll have to duplicate the relevant part of the code in an [ArgumentCompleter] attribute:

[CmdletBinding()]
param (
  [Parameter(Mandatory)]
  [ValidateScript(
    { $_ -in (Get-ChildItem -Directory).Name },
    ErrorMessage = 'Please specify the name of a subdirectory in the current directory.'
  )]
  [ArgumentCompleter(
    {
      param($cmd, $param, $wordToComplete)
      # This is the duplicated part of the code in the [ValidateScipt] attribute.
      [array] $validValues = (Get-ChildItem -Directory).Name
      $validValues -like "$wordToComplete*"
    }
  )]
  [String] $ProjectName # ...
)

Upvotes: 15

Related Questions