Search code examples
powershellargumentsget-childitempowershell-corepowershell-7.3

PowerShell | Argument completer not working when a file with the required criteria is not in the current working directory


I've tried 2 argument completers so far for my module to only show me the files with .cer extension when I press TAB button.

Here is one of them

$ArgumentCompleterCertPath = {
    Get-ChildItem | where-object { $_.extension -like '*.cer' } | foreach-object { return "`"$_`"" }   
}
Register-ArgumentCompleter -CommandName "Edit-SignedWDACConfig" -ParameterName "CertPath" -ScriptBlock $ArgumentCompleterCertPath

And here is the other one

[ArgumentCompleter({
            param($commandName, $parameterName, $wordToComplete, $commandAst)
      
            # Get the current path from the command
            $path = $commandAst.CommandElements |
              Where-Object { $_.ParameterName -eq 'Path' } |
              Select-Object -ExpandProperty Argument
      
            # Resolve the path to a full path
            $resolvedPath = Resolve-Path -LiteralPath $path
      
            # Get the files with .cer extension in the resolved path
            $files = Get-ChildItem -LiteralPath $resolvedPath -Filter *.cer
      
            # Create an array of completion results from the file names
            $completions = foreach ($file in $files) {
              [System.Management.Automation.CompletionResult]::new($file.Name, $file.Name, 'ParameterValue', $file.Name)
            }
      
            # Return the completions
            return $completions
          })]

They both work when there is a certificate file in the current directory, but if there isn't any, they suggest any files with any extensions, even folders, when I press TAB.

What I want from them is:

  1. Do not show wrong files if there isn't any files that meets the argument completer's criteria
  2. if the current working directory doesn't contain any files that meets the argument completer's criteria, recursively search sub-directories to find a good file and even if recursive search didn't find any file, do not show wrong files when TAB is pressed.

For my specific situation, I put the certificate file inside the 2nd sub-directory of the current working directory.

I tried adding

Get-ChildItem -Recurse -ErrorAction 'SilentlyContinue'

to them but that didn't fix it.


Solution

    • By default, PowerShell falls back to its default tab-completion whenever a custom completer returns nothing (which is technically the [System.Management.Automation.Internal.AutomationNull]::Value singleton), which makes it perform the usual completion of all file and subdirectory names in the current directory.

      • You can suppress this fallback behavior if you return $null instead, so that if your custom completer found no matches, no completions are offered at all.

        • Caveat: As of of PowerShell 7.3.4, this seemingly only works with Register-ArgumentCompleter calls, not with parameter-individual ArgumentCompleter attributes using script blocks, and it isn't clear if this an officially supported mechanism or if there even is one.

        • However, as Santiago Squarzon has discovered, fallback can also be prevented if you use the ArgumentCompleter attribute with a custom class that implements IArgumentCompleter rather than with a script block.

          • In fact, the logic is reversed then: use return $null as an opt-in to falling back to default completion.
          • See Santiago's sample code.
      • As an aside, as of PowerShell 7.3.4:

        • PowerShell's default tab-completion is overzealous in that it performs file/directory-name completion even for parameter types where that doesn't make any sense (e.g. [int]) - see GitHub issue #14147
    • As Santiago points out, implementing unconstrained use of Get-ChildItem -Recurse in a tab-completer is problematic, as it could result in a potentially prohibitively lengthy operation; however, you can use the -Depth parameter to limit the recursion to a fixed depth.

      • E.g., -Depth 1 limits the lookups to the content of the input directory itself and that of its immediate subdirectories.

    Therefore, try something like the following:

    $ArgumentCompleterCertPath = {
      # Note the use of -Depth 1
      # Enclosing the $results = ... assignment in (...) also passes the value through.
      ($results = Get-ChildItem -Depth 1 -Filter *.cer | foreach-object {  "`"$_`"" })
      if (-not $results) { # No results?
        $null # Dummy response that prevents fallback to the default file-name completion.
      } 
    }
    Register-ArgumentCompleter -CommandName "Foo" -ParameterName "CertPath" -ScriptBlock $ArgumentCompleterCertPath