0

I'm not sure if it's possible to do this so going to first explain what I want to happen.

my PowerShell module function has this parameter

[ValidateScript({ Test-Path $_ -PathType Leaf })][ValidatePattern("\.xml$")][parameter(Mandatory = $true)][string[]]$PolicyPaths,

It accepts multiple .xml files.

I've been using this argument completer for it:

$ArgumentCompleterPolicyPaths = {
    Get-ChildItem | where-object { $_.extension -like '*.xml' } | foreach-object { return "`"$_`"" }
}
Register-ArgumentCompleter -CommandName "Deploy-SignedWDACConfig" -ParameterName "PolicyPaths" -ScriptBlock $ArgumentCompleterPolicyPaths

It's been working fine. Now I want to improved it so that when I need to select multiple .xml files from the current working directory, and start selecting them by pressing Tab (without typing anything because I don't know the file names), the suggested files don't contain the ones I've already selected. The way I select them is by pressing Tab first and after each selected file, add a comma , and then press Tab again to select another from the suggestions.

I've tried 2 new argument completers but none of them work the way I want.

here is the first one:

$ArgumentCompleterPolicyPaths = {
  # Get the current command and the already bound parameters
  param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
  # Get the xml files in the current directory
  Get-ChildItem | Where-Object { $_.Extension -like '*.xml' } | ForEach-Object {
    # Check if the file is already selected
    if ($fakeBoundParameters.PolicyPaths -notcontains $_.FullName) {
      # Return the file name with quotes
      return "`"$_`""
    }
  }
}
Register-ArgumentCompleter -CommandName "Deploy-SignedWDACConfig" -ParameterName "PolicyPaths" -ScriptBlock $ArgumentCompleterPolicyPaths

and here is the second one:

# Define a class that inherits from ArgumentCompleterAttribute
class XmlFileCompleter : ArgumentCompleterAttribute {
  # Override the GetArgumentCompletionSuggestions method
  [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] GetArgumentCompletionSuggestions(
    [System.Management.Automation.CommandAst]$commandAst,
    [System.Management.Automation.CommandParameterAst]$parameterAst,
    [System.Collections.IDictionary]$fakeBoundParameters,
    [System.Management.Automation.Language.IScriptPosition]$cursorPosition
  ) {
    # Get all XML files in the current directory
    $xmlFiles = Get-ChildItem -Path . -Filter *.xml
    # Filter out the files that have already been selected
    $xmlFiles = $xmlFiles | Where-Object { $fakeBoundParameters[$parameterAst.ParameterName] -notcontains $_.Name }
    # Return the file names as completion results
    foreach ($xmlFile in $xmlFiles) {
      [System.Management.Automation.CompletionResult]::new($xmlFile.Name, $xmlFile.Name, 'ParameterValue', $xmlFile.Name)
    }
  }
}

The last throws this error in VS code:

Unable to find type [ArgumentCompleterAttribute].

P.S This is current behavior with my argument tab completer, see how it suggests the same file that I already selected: https://1drv.ms/u/s!AtCaUNAJbbvIhupw8-67jn6ScaBOGw?e=a35FoG

it's APNG file so just drag n drop it on a browser.

mklement0
  • 382,024
  • 64
  • 607
  • 775
SpyNet
  • 323
  • 8

1 Answers1

1
  • The problem is that the dictionary of provisionally bound parameters, $fakeBoundParameters, only contains information about other parameters if you haven't typed a prefix of a non-initial array element to be completed.

    • That is, with el1 and el2 representing previously typed / tab-completed array elements, pressing Tab after el1, el2, causes the parameter at hand not to be included as an entry in $fakeBoundParameters, so that the el1 and el2 values cannot be examined that way.

      • This is due to a bug / design limitation of the $fakeBoundParameter dictionary passed to argument-completers, up to at least PowerShell 7.4.0-preview.3: a syntactically incomplete expression such as el1, el2, seemingly prevents inclusion of the array elements provided so far; see GitHub issue #17975
    • Therefore, for an array parameter being tab-completed without having typed at least one character at the start of the new element, any array elements previously typed / completed are in effect not reflected in $fakeBoundParameters.

  • The workaround is to search the command AST passed reflected in the $commandAst completer script-block for string-constant expressions that end end in .xml, as shown below:

    • Note:
      • At least hypothetically, this could yield false positives, namely if other parameters too accept strings ending in .xml
      • As in your own attempt, what the user has manually typed before attempting tab-completion of a given array element is not considered; doing so would require more work.
Register-ArgumentCompleter `
  -CommandName Deploy-SignedWDACConfig `
  -ParameterName PolicyPaths `
  -ScriptBlock {
    # Get the current command and the already bound parameters
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    # Find all string constants in the AST that end in ".xml"
    $existing = $commandAst.FindAll({ 
        $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst] -and 
        $args[0].Value -like '*.xml' 
      }, 
      $false
    ).Value  

    # Get the xml files in the current directory
    Get-ChildItem -Filter *.xml | ForEach-Object {
      # Check if the file is already selected
      if ($_.FullName -notin $existing) {
        # Return the file name with quotes
        "`"$_`""
      }
    }
  }

As for your attempt to use ArgumentCompleterAttribute:

  • You got an error, because you didn't use the full type name as the base class name in your custom class definition, System.Management.Automation.ArgumentCompleterAttribute

    • Curiously, however, omitting the Attribute suffix, i.e. ArgumentCompleter does seem to work without a namespace qualifier.
  • That said, instead of sub-classing this ArgumentCompleter, you could simply use it directly, passing the same script block you used with Register-ArgumentCompleter as an attribute property; that is, inside your Deploy-SignedWDACConfig command, you can decorate the $PolicyPaths parameter declaration with [ArgumentCompleter({ ... })].

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you for the script and info on the other attempt, that works perfectly! looking at my module I don't think I have cases where 2 parameters need `xml` on the same cmdlet, but if that happens I hope it won't be too hard to fix that case . :) – SpyNet Apr 30 '23 at 18:41
  • 1
    Glad to hear it, @SpyNet; my pleasure. Re handling two parameters that accept `.xml` paths: it won't be trivial, unfortunately (you'd need to both recognize parameter names and infer positional arguments), but we can cross that bridge when we get to it; if so, I encourage you to ask a new question that builds on this one. – mklement0 Apr 30 '23 at 18:43
  • Of course ^^ I created a [new question](https://stackoverflow.com/questions/76143006/how-to-prevent-powershell-validateset-argument-completer-from-suggesting-the-sam) but this time I want to apply the same concept on a class-based validateSet argument completer. – SpyNet Apr 30 '23 at 18:57
  • 1
    Thank you, I see the workaround is already implemented in your answer and it's been working really well so far! – SpyNet May 05 '23 at 09:00
  • 1
    Glad to hear it, @SpyNet. When i originally implemented the workaround, the stated reason for why it was needed to begin with wasn't quite correct; the later update I notified you of corrected that. – mklement0 May 05 '23 at 11:56