Skip to content

Instantly share code, notes, and snippets.

@backerman
Last active April 5, 2024 06:30
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save backerman/2c91d31d7a805460f93fe10bdfa0ffb0 to your computer and use it in GitHub Desktop.
Save backerman/2c91d31d7a805460f93fe10bdfa0ffb0 to your computer and use it in GitHub Desktop.
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

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

@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

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

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

@timotheemoulin
Copy link

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.

@thiennn-neji
Copy link

thiennn-neji commented Nov 16, 2022

Thank you so much everyone. This is my version that can read "Include" keyword recursively and support glob too

using namespace System.Management.Automation

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

	function Get-SSHHostList($sshConfigPath) {
		Get-Content -Path $sshConfigPath `
		| Select-String -Pattern '^Host ' `
		| ForEach-Object { $_ -replace 'Host ', '' } `
		| ForEach-Object { $_ -split ' ' } `
		| Sort-Object -Unique `
		| Select-String -Pattern '^.*[*!?].*$' -NotMatch
	}
	
	function Get-SSHConfigFileList ($sshConfigFilePath) {
		$sshConfigDir = Split-Path -Path $sshConfigFilePath -Resolve -Parent
	
		$sshConfigFilePaths = @()
		$sshConfigFilePaths += $sshConfigFilePath
	
		$pathsPatterns = @()
		Get-Content -Path $sshConfigFilePath `
		| Select-String -Pattern '^Include ' `
		| ForEach-Object { $_ -replace 'Include ', '' }  `
		| ForEach-Object { $_ -replace '~', $Env:USERPROFILE } `
		| ForEach-Object { $_ -replace '\$Env:USERPROFILE', $Env:USERPROFILE } `
		| ForEach-Object { $_ -replace '\$Env:HOMEPATH', $Env:USERPROFILE } `
		| ForEach-Object { 
		$sshConfigFilePaths += $(Get-ChildItem -Path $sshConfigDir\$_ -File -ErrorAction SilentlyContinue -Force).FullName `
		| ForEach-Object { Get-SSHConfigFileList $_ } 
		}
	
		if (($sshConfigFilePaths.Length -eq 1) -and ($sshConfigFilePaths.item(0) -eq $sshConfigFilePath) ) {
			return $sshConfigFilePath
		}
	
		return $sshConfigFilePaths | Sort-Object -Unique
	}

	$sshPath = "$Env:USERPROFILE\.ssh"
	$hosts = Get-SSHConfigFileList "$sshPath\config" `
	| ForEach-Object { Get-SSHHostList $_ } `

	# 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, $_) }
}

@ransur0t
Copy link

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

Thanks @hoang-himself -- this snippet works perfectly for my use case, much appreciated.

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