I have 2 class-based argument completers/validate sets for 2 of my PowerShell module's parameters.

[ValidateSet([PolicyIDz])][parameter(Mandatory = $false, ParameterSetName = "Remove Policies")][string[]]$PolicyIDs,
[ValidateSet([PolicyNamez])][parameter(Mandatory = $false, ParameterSetName = "Remove Policies")][string[]]$PolicyNames,
# argument tab auto-completion and ValidateSet for Policy names 
Class PolicyNamez : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        $PolicyNamez = ((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsSystemPolicy -ne "True" }).Friendlyname
           
        return [string[]]$PolicyNamez
    }
}   
    
# argument tab auto-completion and ValidateSet for Policy IDs     
Class PolicyIDz : System.Management.Automation.IValidateSetValuesGenerator {
    [string[]] GetValidValues() {
        $PolicyIDz = ((CiTool -lp -json | ConvertFrom-Json).Policies | Where-Object { $_.IsSystemPolicy -ne "True" }).policyID
           
        return [string[]]$PolicyIDz
    }
}

They are for Windows Defender Application Control and if you want to try it you need at least Windows 11 22H2 which has CITool built-in.

I want to have both validate set and argument completion for each of those parameters, and on top of that, prevent argument completer from suggesting the same values that I've already selected. I'm using latest PowerShell 7.4 version. Both of those parameters are used in the same cmdlet.

Remove-WDACConfig [-RemovePolicies] [-PolicyIDs <String[]>] [-PolicyNames <String[]>]

This question is related to another one I asked previously (and got answers).


This is the current behavior I'm trying to change

https://1drv.ms/u/s!AtCaUNAJbbvIhupxuJSHh3kSkBNTxw?e=KAtshL

1

There are 1 best solutions below

3
On BEST ANSWER
  • As in the accepted answer to your previous question, analysis of the command AST inside an argument-completer script block is (unfortunately) required in order to reliably account for the array elements typed / tab-completed so far, due to a bug / design limitation of the $fakeBoundParameter dictionary passed to argument-completers, up to at least PowerShell 7.4.0-preview.3; see GitHub issue #17975

  • To achieve the desired behavior:

    • [ValidateSet()] attributes can not be used, because they preempt [ArgumentCompleter()] attributes and invariably always offer all valid values, irrespective of which ones have already been specified as part of the array argument at hand.

    • Instead, [ArgumentCompleter()] attributes that implement the desired exclude-what-was-already-specified logic must be complemented with [ValidateScript()] attributes that enforces that only valid values were specified (given that the user may have manually typed invalid ones).


The following is a simplified, self-contained example that uses hard-coded policy IDs and names and defines function Foo with 2 (positional) parameters that tab-complete as desired.

  • Note:

    • For simplicity, what the user has manually typed before attempting tab-completion of a given array element is not considered; doing so would require more work.

    • The AST analysis simply extracts all string constants that have been provided as arguments so far, across all parameters, but at least in the case at hand that isn't problematic, because:

      • The valid values for the two parameters are distinct.
      • The constants are matched against the parameter-appropriate values using Compare-Object, and only those not already present are offered as candidates.
# Argument tab auto-completion and ValidateSet for Policy names.
Class PolicyNamez : System.Management.Automation.IValidateSetValuesGenerator {
  [string[]] GetValidValues() {
    # Use *hard-coded values for this sample code.
    return [string[]] ("VerifiedAndReputableDesktopFlightSupplemental", "VerifiedAndReputableDesktopEvaluationFlightSupplemental", "WindowsE_Lockdown_Flight_Policy_Supplemental", "Microsoft Windows Driver Policy")
  }
}   
  
# Argument tab auto-completion and ValidateSet for Policy IDs.
Class PolicyIDz : System.Management.Automation.IValidateSetValuesGenerator {
  [string[]] GetValidValues() {
    # Use *hard-coded values for this sample code.
    return [string[]] ("1658656c-05ed-481f-bc5b-ebd8c091502d", "2698656d-05ea-481c-bc5b-ebd8c991802d", "5eaf656c-29ad-4a12-ab59-648917362e70", "d2bda972-cdf9-4364-ac5d-0b44497f6816")
  }
}

# Sample function.
function Foo {
  [CmdletBinding()]
  param(
    [ArgumentCompleter({
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
        $candidates = [PolicyIDz]::new().GetValidValues()
        $existing = $commandAst.FindAll({ 
            $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
          }, 
          $false
        ).Value  
        Compare-Object -PassThru $candidates $existing | Where-Object SideIndicator -eq '<='
      })]
    [ValidateScript({
        if ($_ -notin [PolicyIDz]::new().GetValidValues()) { throw "Invalid policy ID: $_" }
        $true
      })]
    [string[]]$PolicyIDs,

    [ArgumentCompleter({
        param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
        $candidates = [PolicyNamez]::new().GetValidValues()
        $existing = $commandAst.FindAll({ 
            $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst]
          }, 
          $false
        ).Value  
      (Compare-Object -PassThru $candidates $existing | Where-Object SideIndicator -eq '<=').
        ForEach({ if ($_ -match ' ') { "'{0}'" -f $_ } else { $_ } })
      })]
    [ValidateScript({
        if ($_ -notin [PolicyNamez]::new().GetValidValues()) { throw "Invalid policy name: $_" }
        $true
      })]
    [string[]]$PolicyNames
  )
}