Search code examples
powershelltab-completiontabexpansion

How to stop PsReadline from appending '.exe' to executables it suggests when tab is pressed


For example, when I type wg and then press tab key, I get wget.exe, I would instead like to get just wget. while I am happy with most things this one feature really annoys me.

Even worse, when I try to modify a previously run line, or correcting something, I am finding myself constantly deleting letters that should not have been there...For example, editing the following first line to become the second line:

wget.exe -O - https://www.voidtools.com/forum/viewtopic.php?t=10860) |html2text.exe
wget -O - https://www.voidtools.com/forum/viewtopic.php?t=10860) |html2text

Okay, the above is not the best example but its what I can think of on the spot. Also this is causing me to declare unnecessary aliases all over the place just to escape this ".exe":

...
Set-Alias -Name Edge               -value "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
Set-Alias -Name Calibre            -value "C:\Program Files\Calibre2\calibre.exe"
Set-Alias -Name CalibreConvert     -value "C:\Program Files\Calibre2\ebook-convert.exe"
Set-Alias -Name Simplewall         -value "C:\Program Files\simplewall\simplewall.exe"
Set-Alias -Name TeraCopy           -value "C:\Program Files\TeraCopy\TeraCopy.exe"
...
Set-Alias -Name MSPaint            -value "C:\Program Files\WindowsApps\Microsoft.Paint_11.2110.0.0_x64__8wekyb3d8bbwe\PaintApp\mspaint.exe"
Set-Alias -Name Diagrams           -value "https://app.diagrams.net/"
Set-Alias -Name ZBrush             -value "C:\Program Files\Pixologic\ZBrush 2021\ZBrush.exe"
Set-Alias -Name Synology           -value "C:\Program Files (x86)\Synology\SynologyDrive\bin\launcher.exe"
Set-Alias -Name WireGuard          -value "C:\Program Files\WireGuard\wireguard.exe"
...

Of course PowerShell accepts wget, some of you may say this is dangerous, I don't really care, its my shell and system, I know how it works. I believe on Linux systems, you get just the name auto completed, that is exactly how I want it...

Has anyone else ever solved this issue??

Of course, I should like this limited to just executables and not effect file names.

I am on Pwsh 7.4, windows 11

Edit: wget is actually a external command, it is a widnows release of the old linux tool, wget.


Solution

  • This isn't related to PSReadLine, it's related to TabExpansion2.

    (Get-Command TabExpansion2).Definition
    

    The reason you get wget.exe from wget is due to the CommandCompletion Class used in the function to provide completion results when you press TAB, i.e.:

    PS ..\pwsh> [System.Management.Automation.CommandCompletion]::CompleteInput('wing', 4, $null).CompletionMatches
    
    # CompletionText ListItemText ResultType ToolTip
    # -------------- ------------ ---------- -------
    # winget.exe     winget.exe      Command C:\path\to\my\winget.exe
    

    Now, I'm totally opposed to the idea of removing that annoying .exe at the end of external commands but since you're asking and as you've said, it's your shell and system, the solution to your problem is to either create new instances of CompletionResult or to update the CompletionText Property for each completion match, the latter option requires reflection. If you agree to that, then you can add this version of TabExpansion2 to your $Profile to override its default behavior.

    function TabExpansion2 {
        [CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')]
        [OutputType([System.Management.Automation.CommandCompletion])]
        Param(
            [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)]
            [AllowEmptyString()]
            [string] $inputScript,
    
            [Parameter(ParameterSetName = 'ScriptInputSet', Position = 1)]
            [int] $cursorColumn = $inputScript.Length,
    
            [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)]
            [System.Management.Automation.Language.Ast] $ast,
    
            [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)]
            [System.Management.Automation.Language.Token[]] $tokens,
    
            [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)]
            [System.Management.Automation.Language.IScriptPosition] $positionOfCursor,
    
            [Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)]
            [Parameter(ParameterSetName = 'AstInputSet', Position = 3)]
            [Hashtable] $options = $null
        )
    
        end {
            if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet') {
                $toComplete = [System.Management.Automation.CommandCompletion]::CompleteInput(
                    <#inputScript#>  $inputScript,
                    <#cursorColumn#> $cursorColumn,
                    <#options#>      $options)
            }
            else {
                $toComplete = [System.Management.Automation.CommandCompletion]::CompleteInput(
                    <#ast#>              $ast,
                    <#tokens#>           $tokens,
                    <#positionOfCursor#> $positionOfCursor,
                    <#options#>          $options)
            }
    
            if (-not $_field) {
                if ($IsCoreCLR) {
                    $fieldName = '_completionText'
                }
                else {
                    $fieldName = 'completionText'
                }
    
                $script:_field = [System.Management.Automation.CompletionResult].GetField(
                    $fieldName,
                    [System.Reflection.BindingFlags] 'Instance, NonPublic')
            }
    
            foreach ($item in $toComplete.CompletionMatches) {
                if ($item.CompletionText.EndsWith('.exe', [StringComparison]::InvariantCultureIgnoreCase)) {
                    $_field.SetValue(
                        $item,
                        [System.IO.Path]::GetFileNameWithoutExtension($item.CompletionText))
                }
            }
    
            $toComplete
        }
    }