Skip to content

Instantly share code, notes, and snippets.

@backerman
Last active November 5, 2025 14:34
Show Gist options
  • 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, $_) }
}
@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.

@vl-tech
Copy link

vl-tech commented Oct 17, 2024

Awesome! It works perfectly! Thanks you! This reminds me to renew my Shell scripting

@hpawe01
Copy link

hpawe01 commented Nov 20, 2024

If you want to simplify the script and just want to use the host-aliases without the user@ part in your ssh command, you can use the following script:

using namespace System.Management.Automation

$script = {
  param($wordToComplete)    

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

Register-ArgumentCompleter -CommandName ssh,scp,sftp -ScriptBlock $script

This autocompletes for example ssh du⭾ to ssh dummy which will internally calls ssh [email protected], given the following ~\.ssh\config:

Host dummy
  Hostname dummy.com
  User admin

@RensTillmann
Copy link

To state the "obvious" but for new users (and for educational purposes) note that this script should be placed in your profile file e.g:

C:\Users\Bob\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

If you have VS code installed you could run code $PROFILE and it should look like:

using namespace System.Management.Automation

# Any other imports you had before
Import-Module posh-sshell

# Then the SSH completion script
Register-ArgumentCompleter -CommandName ssh,scp,sftp -Native -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)
    
    # ... rest of the script
}

# Any other stuff you had in your profile

@DenebTM
Copy link

DenebTM commented Nov 5, 2025

i found this and it was a great starting point, thanks a lot to the people above :3
i then sunk a little too much time into enhancing it... so here's my version, complete with scp remote-path completion:

using namespace System.Management.Automation

function Get-SSH-Hosts {
	param (
		[string]$filename = "${env:USERPROFILE}\.ssh\config"
	)

	if ([System.IO.File]::Exists($filename)) {
		Get-Content $filename `
			| Select-String -Pattern '^(Include|Host) ' `
			| ForEach-Object {
				if ($_ -match '^Include ') {
					$included = (Join-Path $filename .. ($_ -replace '^Include ', '') -Resolve)
					Get-SSH-Hosts $included
				} elseif (!($_ -match '\*')) {
					($_ -replace '^Host ', '' -split ' ')[0]
				}
			} `
			| Select-Object -Unique
	}
}

function Get-SSH-KnownHosts {
	Get-Content ${env:USERPROFILE}\.ssh\known_hosts `
		| ForEach-Object { ([string]$_).Split(' ')[0] } `
		| ForEach-Object { $_.Split(',') }
}

Function Complete-SSH-Host {
	param($wordToComplete, $hosts, $generateCompletionText)

	$textToComplete = $wordToComplete
	# preserve username if specified; only complete hostname
	if ($wordToComplete -match '^(?<user>[-\w/\\]+)@(?<host>[-.\w]+)$') {
		$textToComplete = $Matches['host']
		$generateCompletionText = {
			param($hostname)
			"$($Matches['user'])@${hostname}:"
		}
	}

	$allMatches = @()

	$prefixMatch = @($hosts | Where-Object { $_ -like "${textToComplete}*" })
	$allMatches += $prefixMatch

	$substringMatch = @($hosts | Where-Object { $_ -notin $allMatches -and $_ -like "*${textToComplete}*" })
	$allMatches += $substringMatch

	$fuzzyMatchString = ($textToComplete.toCharArray() | Join-String -OutputPrefix '*' -Separator '*' -OutputSuffix '*')
	# not quite smart-casing, but matches with same case shall be offered first
	$fuzzyMatchCased = @($hosts | Where-Object { $_ -notin $allMatches -and $_ -clike $fuzzyMatchString })
	$allMatches += $fuzzyMatchCased
	$fuzzyMatchUncased = @($hosts | Where-Object { $_ -notin $allMatches -and $_ -like $fuzzyMatchString })
	$allMatches += $fuzzyMatchUncased

	$allMatches | ForEach-Object { [CompletionResult]::new((&$generateCompletionText($_)), $_, [CompletionResultType]::ParameterValue, $_) }
}

Register-ArgumentCompleter -CommandName ssh,sftp -Native -ScriptBlock {
	param($wordToComplete, $commandAst, $cursorPosition)
	$hosts = (Get-SSH-Hosts) + (Get-SSH-KnownHosts) | Sort-Object -Unique

	# for now just assume it's a hostname; no command completion
	$generateCompletionText = {
		param($x)
		$x
	}
	Complete-SSH-Host $wordToComplete $hosts $generateCompletionText
}

Register-ArgumentCompleter -CommandName scp -Native -ScriptBlock {
	param($wordToComplete, $commandAst, $cursorPosition)
	$hosts = (Get-SSH-Hosts) + (Get-SSH-KnownHosts) | Sort-Object -Unique

	if ($wordToComplete -match '^(?<remote>([-\w/\\]+@)?([-.\w]+)):(?<path>.*)$') {
		$remote = $Matches['remote']
		$pathPrefix = $Matches['path'] -replace "^'", "" -replace "([^\\])'$", '$1'
		$generateCompletionText = {
			param($path)

			$quotedPath = $path
			if ($quotedPath -match '\s' -and $quotedPath -notmatch "'") {
				$quotedPath = "`'$quotedPath`'"
			}
			"${remote}:${quotedPath}"
		}

		# 'Write-Host' here is a hack to make password/TOTP prompts appear on a new line
		$files = (Write-Host) + (ssh -oRemoteCommand=none $remote shopt -s dotglob`; ls -1dp "'${pathPrefix}'*")

		return $files `
			| ForEach-Object {
				[CompletionResult]::new((&$generateCompletionText($_)), $_, [CompletionResultType]::ParameterValue, $_)
			}
	}

	$generateCompletionText = {
		param($x)
		"${x}:"
	}
	Complete-SSH-Host $wordToComplete $hosts $generateCompletionText
}

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