Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Couldn't use predefined array inside Validateset - Powershell

I'm looking for a way to make a cmdlet which receives parameter and while typing, it prompts suggestions for completion from a predefined array of options.

I was trying something like this:

$vf = @('Veg', 'Fruit')
function Test-ArgumentCompleter {
  [CmdletBinding()]
    param (
          [Parameter(Mandatory=$true)]
          [ValidateSet($vf)]
          $Arg
    )
}

The expected result should be:
When writing 'Test-ArgumentCompleter F', after clicking the tub button, the F autocompleted to Fruit.

like image 482
barper Avatar asked May 02 '21 13:05

barper


People also ask

How to use the validateset attribute in PowerShell function?

How to use the ValidateSet Attribute in PowerShell function? The ValidateSet attribute in PowerShell function is to validate the values entered from the set which means, it allows only specific values from the set.

Is PowerShell validateset case insensitive?

Making PowerShell ValidateSet Case Sensitive By default, the ValidateSet attribute is case insensitive. This means that it will allow any string granted it’s in the allowed list with any capitalization scheme. For example, the above example will accept Mars just as easily as it would accept mars.

Can I use validateset to test for Intellisense in arrays?

That was to say you can't do what you want with ValidateSet. You can use ValidateScript where you test yourself if the input var is found in an array, but then you lose intellisense. To complement the answers from @mklement0 and @Mathias, using dynamic parameters:

How do I add a validateset to a script parameter?

A ValidateSet list is a comma-separated list of string values, wrapped in single or double-quotes. Adding a ValidateSet attribute to a script or function parameter consists of adding a line of text to the Param () block, as shown below. Replace the Param () block in your copy of Get-PlanetSize.ps1 with the one below and save the file.


Video Answer


4 Answers

To complement the answers from @mklement0 and @Mathias, using dynamic parameters:

$vf = 'Veg', 'Fruit'

function Test-ArgumentCompleter {
    [CmdletBinding()]
    param ()
    DynamicParam {
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        $AttributeCollection.Add($ParameterAttribute)
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($vf)
        $AttributeCollection.Add($ValidateSetAttribute)
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter('Arg', [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add('Arg', $RuntimeParameter)
        return $RuntimeParameterDictionary
    }
}

Depending on how you want to predefine you argument values, you might also use dynamic validateSet values:

Class vfValues : System.Management.Automation.IValidateSetValuesGenerator {
    [String[]] GetValidValues() { return 'Veg', 'Fruit' }
}

function Test-ArgumentCompleter {
[CmdletBinding()]
param (
        [Parameter(Mandatory=$true)]
        [ValidateSet([vfValues])]$Arg
    )
}

note: The IValidateSetValuesGenerator class [read: interface] was introduced in PowerShell 6.0

like image 117
iRon Avatar answered Oct 23 '22 08:10

iRon


  • PowerShell generally requires that attribute properties be literals (e.g., 'Veg') or constants (e.g., $true).

  • Dynamic functionality requires use of a script block (itself specified as a literal, { ... }) or, in specific cases, a type literal.

  • However, the [ValidateSet()] attribute only accepts an array of string(ified-on-demand) literals or - in PowerShell (Core) v6 and above - a type literal (see below).


Update:

  • If you're using PowerShell (Core) v6+, there's a simpler solution based on defining a custom class that implements the System.Management.Automation.IValidateSetValuesGenerator interface - see the 2nd solution in iRon's helpful answer.

  • Even in Windows PowerShell a simpler solution is possible if your validation values can be defined as an enum type - see Mathias R. Jessen's helpful answer.


To get the desired functionality based on a non-literal array of values, you need to combine two other attributes:

  • [ArgumentCompleter()] for dynamic tab-completion.

  • [ValidateScript()] for ensuring on command submission that the argument is indeed a value from the array, using a script block.

# The array to use for tab-completion and validation.
[string[]] $vf = 'Veg', 'Fruit'

function Test-ArgumentCompleter {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    # Tab-complete based on array $vf
    [ArgumentCompleter({
      param($cmd, $param, $wordToComplete) $vf -like "$wordToComplete*"
    })]
    # Validate based on array $vf.
    # NOTE: If validation fails, the (default) error message is unhelpful.
    #       You can work around that in *Windows PowerShell* with `throw`, and in
    #       PowerShell (Core) 7+, you can add an `ErrorMessage` property:
    #         [ValidateScript({ $_ -in $vf }, ErrorMessage = 'Unknown value: {0}')]
    [ValidateScript({
      if ($_ -in $vf) { return $true }
      throw "'$_' is not in the set of the supported values: $($vf -join ', ')"
    })]
    $Arg
  )

  "Arg passed: $Arg"
}
like image 31
mklement0 Avatar answered Oct 23 '22 10:10

mklement0


In addition to mklement0's excellent answer, I feel obligated to point out that in version 5 and up you have a slightly simpler alternative available: enum's

An enum, or an "enumeration type", is a static list of labels (strings) associated with an underlying integral value (a number) - and by constraining a parameter to an enum type, PowerShell will automatically validate the input value against it AND provide argument completion:

enum MyParameterType
{
  Veg
  Fruit
}

function Test-ArgumentCompleter {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [MyParameterType]$Arg
    )
}

Trying to tab complete the argument for -Arg will now cycle throw matching valid enum labels of MyParameterType:

PS ~> Test-ArgumentCompleter -Arg v[<TAB>]
# gives you
PS ~> Test-ArgumentCompleter -Arg Veg
like image 6
Mathias R. Jessen Avatar answered Oct 23 '22 08:10

Mathias R. Jessen


To add to the other helpful answers, I use something similiar for a script I made for work:

$vf = @('Veg', 'Fruit','Apple','orange')

$ScriptBlock = {
    Foreach($v in $vf){
        New-Object -Type System.Management.Automation.CompletionResult -ArgumentList $v, 
            $v, 
            "ParameterValue",
            "This is the description for $v"
    }
}

Register-ArgumentCompleter -CommandName Test-ArgumentCompleter -ParameterName Arg -ScriptBlock $ScriptBlock


function Test-ArgumentCompleter {
[CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String]$Arg )
}

Documentation for Register-ArgumentCompleter is well explained on Microsoft Docs. I personally don't like to use the enum statement as it didnt allow me to uses spaces in my Intellisense; same for the Validate parameter along with nice features to add a description.

Output:

enter image description here

EDIT:

@Mklement made a good point in validating the argument supplied to the parameter. This alone doesnt allow you to do so without using a little more powershell logic to do the validating for you (unfortunately, it would be done in the body of the function).

function Test-ArgumentCompleter {
[CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        $Arg )
      
   if($PSBoundParameters.ContainsKey('Arg')){
       if($VF -contains $PSBoundParameters.Values){ "It work:)" }
           else { "It no work:("}
    }
} 
like image 4
Abraham Zinala Avatar answered Oct 23 '22 10:10

Abraham Zinala