You need to replace the default PowerShell function that's used for tab completion, the mostly-undocumented TabExpansion2
, the content of which you can obtain by running get-content function:global:tabexpansion2
.
Because the content of this function may differ on your system, I'm not going to show it in its entirety, only the pertinent part which is the return of the calculated tab completion possibilities (this is from PowerShell Core 7.3.2 x64 running on Windows 10 21H2 x64):
... start of TabCompletion2...
End
{
if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
{
return [System.Management.Automation.CommandCompletion]::CompleteInput(
<#inputScript#> $inputScript,
<#cursorColumn#> $cursorColumn,
<#options#> $options)
}
else
{
return [System.Management.Automation.CommandCompletion]::CompleteInput(
<#ast#> $ast,
<#tokens#> $tokens,
<#positionOfCursor#> $positionOfCursor,
<#options#> $options)
}
}
Both code paths are calling the static System.Management.Automation.CommandCompletion.CompleteInput
method, using different versions of that method depending on the arguments passed to TabExpansion2
.
At this point you might think that we need to delve into the innards of these methods and tweak them to taste, but thankfully that's not the case. We don't actually need to change how CommandCompletion.CompleteInput
works - we just want to change the order of its suggestions. Since it's already done the hard bit, we just need to do the reordering!
Hence, modify TabCompletion2
to the following:
... start of TabCompletion2...
End
{
if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
<#inputScript#> $inputScript,
<#cursorColumn#> $cursorColumn,
<#options#> $options)
}
else
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
<#ast#> $ast,
<#tokens#> $tokens,
<#positionOfCursor#> $positionOfCursor,
<#options#> $options)
}
$exeMatches, $nonExeMatches = $completion.CompletionMatches
.Where({$_.CompletionText -Like "*.exe"}, "Split")
$completion.CompletionMatches = @($exeMatches) + @($nonExeMatches)
return $completion
}
It's actually really simple:
- we use the magic array
Where
method to filter the CompletionMatches
collection that CommandCompletion
has helpfully already populated for us, splitting it into one part containing the found executable matches and the other part containing the remaining matches
- overwrite the default completion matches with our sorted matches, placing the EXE matches first so they're presented first, and ensuring we use array constructors to overcome PowerShell's propensity for causing problems by silently returning a single item instead of an array
- return the completion
With this updated TabCompletion2
installed into our profile and after reloading said profile via typing .$profile
and pressing Enter, typing .\
and pressing Tab now yields the desired result:
> .\d.exe
> .\z.exe
> .\a.json
> .\b.dll
> .\c.config