1

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

SpyNet
  • 323
  • 8

1 Answers1

1
  • 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
  )
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you so much, fully tested it and works perfectly! To push the boundaries a bit further, would it be possible to cross-examine each suggested arguments? Based on the syntax of the cmdlet in the Q, CI Policies can either be removed by ID or by name. Is it possible to dynamically calculate each suggested argument so that if the ID of a deployed policy is already suggested in the previous parameter, its name don't show up for the next parameter? I assume it's much harder for you to test without having it deployed on a VM. If it's possible, Plz let me know so i can make a new Q for it. Thanks – SpyNet Apr 30 '23 at 20:20
  • Also, what is the best practice for placing argument completion blocks/Validate set Classes etc. like in your previous answer and this one? I put them at the very end of the module file but I see you put it before the main function. P.S I credit your work by adding links to your answers in my change log and also in the module code as comments. – SpyNet Apr 30 '23 at 20:23
  • 1
    Glad to hear it, @SpyNet, and I appreciate your giving me credit. As for cross-referencing between parameters: you should be able to do that via the `$fakeBoundParameters` parameter, which _is_ populated for _other_ parameters. Re placement of helper definitions such as validation classes: I'd say that's a matter of taste. The order only matters for _function_ definitions. – mklement0 Apr 30 '23 at 20:33