Recently, I was talking with one of my good friend, Matthieu, who explained me how he uses, in WAQS, some Nuget packages to add commands to the PowerShell console. As I like this idea, I decide to take a look at it and it’s really powerful!

So let’s see how it works and let me show you a preview of what you can do :)

Creating the Nuget package

This is the main (but mandatory) step you’ll need to do. For that, I decided to use the tool Nuget Package Explorer that let you to create your package, edit its metadata, add some dependencies, etc.

image

Once you created your package (it’s as simple as click on New –> File), you need to specify what you’ll put in your package. In our case, we want to add some commands to the PowerShell console so we need a script that will run every time a solution/project is opened so we have to add some content to the file init.ps1:

image

We could have choose to put our code in the file install.ps1 but the content of this file is only executed when the Nuget package is installed and not when user open your project in a new instance of Visual Studio (because, in that case, there is no install to perform as the package is already installed). As we want our commands to be always available (not only when the package is installed), we need to use init.ps1 (thanks Matthieu for the trick).

On that file, we’ll perform 2 actions: first, we’ll remove all the previous versions of the module we are developing then we’ll import it (using a module is not always needed but it’s a good thing to split your code in smaller files):

param($installPath, $toolsPath, $package)

foreach ($_ in Get-Module | ?{$_.Name -eq 'VSExtensionsModule'})
{
    Remove-Module 'VSExtensionsModule'
}

Import-Module (Join-Path $toolsPath VSExtensionsModule.psm1)

If the first step is not mandatory (unlike the second one), I recommend you to use it because it’ll prevent your package to be loaded more than 1 time! For the second step, it’ll just import our module to the PowerShell console so now, let’s take a look at the content of that module.

Adding commands to the PowerShell console

Our use case is simple: we want a PowerShell command that will allow us to count, in our project, all the files of a particular extension provided on IntelliSense by the files located on the root folder of the project (yes, I know this is not perfect but it’s just for the demo):

image

To do that, we need to register some code to tell PowerShell to show IntelliSense when user press the Tab key:

Register-TabExpansion 'Get-FilesCount' @{
    'extension' = { Get-Extensions | foreach { $_ } }
}

This code can be read as: “when the user write the command Get-FileCount and press the Tab key, use the method Get-Extensions to get the list of all available extensions and display them on screen”. So the next interesting point is to look at the Get-Extensions method:

function Get-Extensions()
{
    $extensionsList = New-Object Collections.Generic.List``1[string]

    $project = Get-Project
    $projectPath = [System.IO.Path]::GetDirectoryName($project.FullName)

    foreach($file in [System.IO.Directory]::GetFiles($projectPath))
    {
        $fileExtension = [System.IO.Path]::GetExtension($file)
        #$fileExtension = $fileExtension.Substring(1)

        if(!$extensionsList.Contains($fileExtension))
        {
            $extensionsList.Add($fileExtension)
        }
    }

    return $extensionsList
}

This method first starts by creating a generic list of string (List<string>) that will be used to store the list of all extensions. Then, by using the Get-Project method, we get the current active project in Visual Studio and, with the Path.GetDirectoryName method, we retrieve the path to the directory of that project! Then, we iterate through all the files of the folder (be careful here because we don’t use a recursive method) and we get the extension of all the files. That’s all, we are now able to display, on the IntelliSense, the list of all the extensions:

image

Ok so now that the IntelliSense is here, we just need to finish to implement the Get-FilesCount method:

function Get-FilesCount($extension)
{
    $project = Get-Project
    $projectPath = [System.IO.Path]::GetDirectoryName($project.FullName)

    $toExclude = @("\bin", "\obj")

    $files = New-Object Collections.Generic.List``1[string]

    GetFiles $projectPath $toExclude $files

    if($extension -eq $null)
    {
       Write-Host "There are "$files.Count" files on the project."
    }
    else
    {
       $count = 0

       foreach($file in $files)
       {
           if ($file.ToLowerInvariant().EndsWith($extension))
           {
                $count++
           }
       }

       Write-Host "There are "$count" files with the extension '"$extension"' on the project."
    }
}

function GetFiles($path, [string[]]$exclude, $files)
{
    foreach ($item in Get-ChildItem $path)
    {
        if ($exclude | Where { $item.FullName.Contains($_) }) { continue }

        if (Test-Path $item.FullName -PathType Container)
        {
            GetFiles $item.FullName $exclude $files
        }
        else
        {
            $files.Add($item.FullName)
        }
    }
}

The content of the method is pretty simple: we use a recursive method to get all the files from the project and we display the number of files available (or the number of files corresponding to a particular extension).

Finally, we need to export the method of the module:

Export-ModuleMember Get-FilesCount

Once the package is installed on the project, we can use the method in the console:

image

This example might be a bit naive so let’s take a look at a more real sample.

A better example: Get-ImagesFileSizes

This time, we’ll build a better command, that will be used to display the list of all the image’s files (and their size) in your project.

Again, we’ll support the IntelliSense:

Register-TabExpansion 'Get-ImagesFileSizes' @{
    'extension' = { Get-ImagesExtensions | foreach { $_ } }
}

The Get-ImagesExtensions is a simple method (it could be possible to build a more complex one, that display the extension of the image’s files available in the project):

function Get-ImagesExtensions()
{
    $imagesExtensionsList = New-Object Collections.Generic.List``1[string]

    $imagesExtensionsList.Add(".png")
    $imagesExtensionsList.Add(".jpg")
    $imagesExtensionsList.Add(".jpeg")
    $imagesExtensionsList.Add(".bmp")
    $imagesExtensionsList.Add(".gif")

    return $imagesExtensionsList
}

Finally, we need to implement the Get-ImagesFileSizes method. This method will be quite similar to the previous one byt, this time, we’ll access to the Length property of the file:

function Get-ImagesFileSizes($extension)
{
    $project = Get-Project
    $projectPath = [System.IO.Path]::GetDirectoryName($project.FullName)

    $toExclude = @("\bin", "\obj")

    $files = New-Object Collections.Generic.List``1[string]

    GetFiles $projectPath $toExclude $files

    if($files.Count -gt 0)
    {
        if($extension -eq $null)
        {
           foreach($file in $files)
           {
               Write-Host $file ":" (Format-FileSize((Get-Item $file).Length))
           }
        }
        else
        {
            $filesWithExtension = ($files | Where { $_.ToLowerInvariant().EndsWith($extension) })
            if($filesWithExtension.Count -gt 0)
            {
                foreach($file in $filesWithExtension)
                {
                    Write-Host $file ": " (Format-FileSize((Get-Item $file).Length))
                }
            }
            else
            {
                Write-Host "Sorry, there are no image(s) with the following extension" $extension
            }
        }
    }
    else
    {
        Write-Host "Humm.... Looks like there are no images on your project :)"
    }
}

The interesting point here is the Format-FileSize method that he use to format the file size to a human readable value:

#
# Source: http://superuser.com/questions/468782/show-human-readable-file-sizes-in-the-default-powershell-ls-command
#
function Format-FileSize()
{
    param ([int]$size)

    if ($size -gt 1TB)
    {
        [string]::Format("{0:0} Tb", $size / 1TB)
    }
    elseif ($size -gt 1GB)
    {
        [string]::Format("{0:0} Gb", $size / 1GB)
    }
    elseif ($size -gt 1MB)
    {
        [string]::Format("{0:0} Mb", $size / 1MB)
    }
    elseif ($size -gt 1KB)
    {
        [string]::Format("{0:0} Kb", $size / 1KB)
    }
    elseif ($size -gt 0)
    {
        [string]::Format("{0:0} b", $size)
    }
    else
    {
        ""
    }
}

When installed and used on the project, the extension works fine:

image

image

 

As you can see, using Nuget/PowerShell to extend the PowerShell console is really easy (if we omits the fact that you may need to learn the PowerShell :) and you can do a lot of more complex things (dynamically adding resources, create base files for a View/ViewModel in XAML projects, etc.). If you want to learn more, I suggest you to look in WAQS, you’ll have a good overview of what’s possible to do!

 

Happy coding!