Skip to content

Instantly share code, notes, and snippets.

@backerman
Last active April 30, 2024 08:58
Show Gist options
  • Star 30 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, $_) }
}
@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