Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Enable tab completion for ssh hostnames in PowerShell
using namespace System.Management.Automation
Register-ArgumentCompleter -CommandName ssh,scp,sftp -Native -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$knownHosts = Get-Content ${Env:HOMEPATH}\.ssh\known_hosts `
| ForEach-Object { ([string]$_).Split(' ')[0] } `
| ForEach-Object { $_.Split(',') } `
| Sort-Object -Unique
# For now just assume it's a hostname.
$textToComplete = $wordToComplete
$generateCompletionText = {
param($x)
$x
}
if ($wordToComplete -match "^(?<user>[-\w/\\]+)@(?<host>[-.\w]+)$") {
$textToComplete = $Matches["host"]
$generateCompletionText = {
param($hostname)
$Matches["user"] + "@" + $hostname
}
}
$knownHosts `
| Where-Object { $_ -like "${textToComplete}*" } `
| ForEach-Object { [CompletionResult]::new((&$generateCompletionText($_)), $_, [CompletionResultType]::ParameterValue, $_) }
}
@elazarcoh
Copy link

elazarcoh commented Jun 1, 2021

Thanks!
I wanted to use with the .ssh/config, So I changed to:

 $hosts = Get-Content ${Env:HOMEPATH}\.ssh\config `
    | Select-String -Pattern "^Host "
    | ForEach-Object { $_ -replace "host ", "" }
    | Sort-Object -Unique

@countzero
Copy link

countzero commented Jun 24, 2021

@backerman Thank you for sharing!

@elazarcoh I also wanted to use it with my SSH config file but additionally support the Include feature AND support host name aliases:

# We are assuming that all SSH config files are named *config and
# are correctly included in the main .ssh/config file.
$hosts = Get-Content $(Get-ChildItem -Path ${Env:HOMEPATH}\.ssh -Filter *config -Recurse).FullName `
| Select-String -Pattern "^Host " `
| ForEach-Object { $_ -replace "host ", "" -split " " } `
| Sort-Object -Unique

And if you want the SSH hostname autocompletion to be persistent for all PowerShell instances you have to paste it to your PowerShell Profile.

Open the PowerShell Profile file for all users on the current host by executing the following with administrator rights in a PowerShell:

if (!(Test-Path -Path $PROFILE.AllUsersCurrentHost)) {
    New-Item -ItemType File -Path $PROFILE.AllUsersCurrentHost -Force
}; 
notepad $PROFILE.AllUsersCurrentHost

@elazarcoh
Copy link

elazarcoh commented Oct 18, 2021

@countzero Support for Include directly. I guess it can be used recursively, if you need it.

using namespace System.Management.Automation

function Get-Hosts($configFile) {
    Get-Content $configFile `
    | Select-String -Pattern "^Host "
    | ForEach-Object { $_ -replace "host ", "" }
    | Sort-Object -Unique
}

Register-ArgumentCompleter -CommandName ssh, scp, sftp -Native -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    $sshDir = "${Env:HOMEPATH}\.ssh"

    $hosts = Get-Content "$sshDir\config" `
    | Select-String -Pattern "^Include "
    | ForEach-Object { $_ -replace "include ", "" }
    | ForEach-Object { Get-Hosts "$sshDir/$_" } `

    $hosts += Get-Hosts "$sshDir\config"
    $hosts = $hosts | Sort-Object -Unique

    # For now just assume it's a hostname.
    $textToComplete = $wordToComplete
    $generateCompletionText = {
        param($x)
        $x
    }
    if ($wordToComplete -match "^(?<user>[-\w/\\]+)@(?<host>[-.\w]+)$") {
        $textToComplete = $Matches["host"]
        $generateCompletionText = {
            param($hostname)
            $Matches["user"] + "@" + $hostname
        }
    }

    $hosts `
    | Where-Object { $_ -like "${textToComplete}*" } `
    | ForEach-Object { [CompletionResult]::new((&$generateCompletionText($_)), $_, [CompletionResultType]::ParameterValue, $_) }
}

@jimmyc802
Copy link

jimmyc802 commented Feb 10, 2022

Does this still work? I've tried adding this to my Powershell profile but no tab completion for any of my ssh known hosts or hosts in the config file.

@elazarcoh
Copy link

elazarcoh commented Feb 10, 2022

I works for me. Maybe you need to load PSReadLine, I'm not really sure.

@timotheemoulin
Copy link

timotheemoulin commented Apr 12, 2022

Just adding a note. There are missing backticks ``` in the example. You need to add them at the end of every line begining with a pipe |.
I think that they have been removed by the markdown parser...

Here is the working version to auto complete the from the .ssh/config file.

using namespace System.Management.Automation

### SSH autocompletion
Function Get-Hosts($configFile) {
    Get-Content $configFile `
    | Select-String -Pattern "^Host " `
    | ForEach-Object { $_ -replace "host ", "" } `
    | Sort-Object -Unique `
}

Register-ArgumentCompleter -CommandName ssh, scp, sftp -Native -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    $sshDir = "${Env:HOMEPATH}\.ssh"

    $hosts = Get-Content "$sshDir\config" `
    | Select-String -Pattern "^Include " `
    | ForEach-Object { $_ -replace "include ", "" } `
    | ForEach-Object { Get-Hosts "$sshDir/$_" } `

    $hosts += Get-Hosts "$sshDir\config"
    $hosts = $hosts | Sort-Object -Unique

    # For now just assume it's a hostname.
    $textToComplete = $wordToComplete
    $generateCompletionText = {
        param($x)
        $x
    }
    if ($wordToComplete -match "^(?<user>[-\w/\\]+)@(?<host>[-.\w]+)$") {
        $textToComplete = $Matches["host"]
        $generateCompletionText = {
            param($hostname)
            $Matches["user"] + "@" + $hostname
        }
    }

    $hosts `
    | Where-Object { $_ -like "${textToComplete}*" } `
    | ForEach-Object { [CompletionResult]::new((&$generateCompletionText($_)), $_, [CompletionResultType]::ParameterValue, $_) }
}

@hoang-himself
Copy link

hoang-himself commented Apr 13, 2022

Thanks to all of you, this is my version that uses ~\.ssh\config, reads Include recursively, accepts multiple hosts per line, and filters out hosts with wildcards

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
param()

function Get-SSHHost($sshConfigPath) {
  Get-Content -Path $sshConfigPath `
  | Select-String -Pattern '^Host ' `
  | ForEach-Object { $_ -replace 'Host ', '' } `
  | ForEach-Object { $_ -split ' ' } `
  | Sort-Object -Unique `
  | Select-String -Pattern '^.*[*!?].*$' -NotMatch
}

Register-ArgumentCompleter -CommandName 'ssh', 'scp', 'sftp' -Native -ScriptBlock {
  param($wordToComplete, $commandAst, $cursorPosition)

  $sshPath = "$env:USERPROFILE\.ssh"

  $hosts = Get-Content -Path "$sshPath\config" `
  | Select-String -Pattern '^Include ' `
  | ForEach-Object { $_ -replace 'Include ', '' }  `
  | ForEach-Object { Get-SSHHost "$sshPath/$_" }

  $hosts += Get-SSHHost "$sshPath\config"
  $hosts = $hosts | Sort-Object -Unique

  $hosts | Where-Object { $_ -like "$wordToComplete*" } `
  | ForEach-Object { $_ }
}

I don't use known_hosts and I don't understand why some people use it, but I made this anyway

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')]
param()

function Get-SSHKnownHost($sshKnownHostsPath) {
  Get-Content -Path $sshKnownHostsPath `
  | ForEach-Object { $_.split(' ')[0] } `
  | Sort-Object -Unique
}

Register-ArgumentCompleter -CommandName 'ssh', 'scp', 'sftp' -Native -ScriptBlock {
  param($wordToComplete, $commandAst, $cursorPosition)

  $hosts = Get-SSHKnownHost "$env:USERPROFILE\.ssh\known_hosts"

  if ($wordToComplete -match '^(?<user>[-\w/\\]+)@(?<host>[-.\w]+)$') {
    $hosts | Where-Object { $_ -like "$($Matches['host'].ToString())*" } `
    | ForEach-Object { "$($Matches['user'].ToString())@$_" }
  }
}

I imagine you can use both of these at the same time, but pattern matching must be changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment