If you subscribe to my blog or follow me on Twitter you'll probably know I'm quite a fan of NuGet. Recently I was playing around with Andrew Nurse's PSGet module that wraps NuGet.exe for PowerShell (which, by the way, is an awesome idea, and should be added as built-in functionality!). I decided a good way to learn a little more about PowerShell would be to try and add Tab Expansion to PSGet, similar to the functionality in the Visual Studio Package Manager Console.

Note: I forked Andrew Nurse's PSGet project to make these changes, though I'll see if he's interested in pulling them back into his, and updating the NuGet package. That way people will have a single source for PowerShell/NuGet goodness.

Disclaimer: I've only been using PowerShell for around a week, so I'm sure there are much better ways to do what I've done. I did what I could with the (little) knowledge I have and some Googling. If you can see better ways of doing things, please let me know so I can learn, and update my code (send me a pull request if you want).

Show me the code!

Tab Expansion in PowerShell works via the TabExpansion method. This function is called and provided with the current line in addition to the last word typed. I couldn't find a way to hook TabExpansion for just the commands we're interested in, so I had to hijack the whole lot. This doesn't seem to be a problem, as falling back to the default behaviour seemed pretty simple by keeping a copy of the function:

# Copy the current Tab Expansion function so we can fall back to it if we're not supposed to handle a command
Copy Function:\TabExpansion Function:\OriginalTabExpansion

function TabExpansion([string] $line, [string] $lastword)
{
	# Only run for "Get-PSPackage" or "Install-PSPackage"
	if (somecondition)
	{
		# Do cool NuGet stuff here
	}
	# Otherwise, fall back to default TabExpansion function
	else
	{
		OriginalTabExpansion $line $lastword
	}
}
Export-ModuleMember -Function TabExpansion

We need our NuGet code to run only for two specific commands, for the first parameter. The easiest way seemed to be to check that $line is equal to one of our commands followed by the $lastword. This probably isn't the best way, but it seems to do the job for the obvious cases.

# Only run for "Get-PSPackage" or "Install-PSPackage" where we're typing the first argument
if ($line -eq "Get-PSPackage $lastword" -or $line -eq "Install-PSPackage $lastword")
{
	# ...
}

Inside this block, we need to query the OData feed for NuGet packages. Rather than hard-coding the URI, we're supposed to go through a Microsoft fwlink (https://go.microsoft.com/fwlink/?LinkID=206669), but because we can't append parameters to the end, we need to resolve this redirect. I found some code in the NuGet issue tracker written by David Ebbo to do just this. I converted it to PowerShell and added a $defaultSource variable:

# Follows redirects and returns the final URI. Used to support fwlink as package source.
function GetRedirectedUri($uri)
{
	$req = [System.Net.HttpWebRequest] [System.Net.WebRequest]::Create($uri)
	$req.AllowAutoRedirect = $FALSE; # Don't follow redirects, to save bandwidth/roundtrip
	$resp = $req.GetResponse()
	if ($resp.StatusCode -eq [System.Net.HttpStatusCode]::Redirect)
	{
		$resp.Headers["Location"]
	}
	else
	{
		$uri;
	}
}
# Set the default NuGet source
$defaultSource = GetRedirectedUri("https://go.microsoft.com/fwlink/?LinkID=206669")

With this done, we just needed to call the feed with some search parameters and parse the results. Luckily, parsing OData is pretty trivial, and with Fiddler and Visual Studio, it was easy to find the syntax for searching for packages with IDs that start with a given string :)

The final step was to pipe through Get-Unique because the OData feed returns multiple entries for packages with multiple versions (but the ID is the same):

# Construct a URI to fetch the top 30 matching packages, sorted by download count
$uri = "$($defaultSource)Packages()?`$filter=startswith(tolower(Id),'$($lastword)')&`$orderby=DownloadCount%20desc,Id&$`skip=0&`$top=30"

# Fetch the data, return as XML
$wc = New-Object Net.WebClient
$data = [xml]$wc.DownloadString($uri)

# Pipe result through Get-Unique because there could be multiple versions with the same ID
$(
	# Loop, just grabbing the ID from Properties
	foreach ($entry in $data.feed.entry)
	{
		$entry.properties.Id
	}
) | Get-Unique

And viola, Tab Expansion! Now when you type "Install-PSPackage x" or "Get-PSPackage x" you can use tab to cycle through the matching packages (sorted by download count, in an attempt to give the most likely result first).

Maybe with a little tidying up we can get Andrew Nurse to pull the functionality into his repository and update PSGet on NuGet.