PowerShell ISE + TFS

For a while now I have been using a combination of ISE and Visual Studio to maintain my powershell scripts. Why? because I wanted to be able to have proper version control for my team and also have the features supplied by ISE.

Enter Visual Studio Team Foundation Server 2013 Update 2 Power Tools. They come with a PSSnapin to maintain files on TFS. The Scripting Guys have a great detailed blog on TFS and the PowerShell SnapIn here.

Please note that 2013 is now available and the process is very much the same. In brief, you can signup for a free TFS service via Visual Studio Online. Then Install the TFS 2013 Client. Configure the client. Install Power Tools for TFS 2013.

Now you have got TFS, ISE and a bunch of cmdlets, you might be wondering, is this really going to be easier than managing it via the VS gui? Well with a few customizations to my ISE profile and it will be.

After installing the TFS Power Tools, add the following to your ISE profile. (Just modify the $Global:WorkspacePath Variable to suit your environment).

This will make sure you have the latest files from TFS each time you load ISE, and will add a menu under add-ons where you can check-out the current file, and check-in all pending files.

Stay posted as I will publish a standalone module for easier installation soon.

$Global:WorkspacePath = 'Path/to/VS/Workspace'
Add-Type -AssemblyName System.Windows.Forms
Add-PSSnapin -Name Microsoft.TeamFoundation.PowerShell

function Checkin-Files
{
    Add-TfsPendingChange -Add -Item $Global:WorkspacePath -Recurse
    $pendingFiles = Get-TfsPendingChange -Item $Global:WorkspacePath -Recurse
    if ($pendingFiles -ne $null)
    {
        $TFSCheck = [System.Windows.Forms.MessageBox]::Show(($pendingFiles | Select-Object -ExpandProperty localitem),'Do you want to check in these files?', 'TFS Check in','YesNo')
        if ($TFSCheck -eq 'Yes') 
        {
            $TFSResult = New-TfsChangeset -Item $Global:WorkspacePath -Recurse 
            if ($TFSResult -ne $null){
            $null = [System.Windows.Forms.MessageBox]::Show(('Change Set ID: ' + $TFSResult.ChangesetID + "`r`nOwner: " + $TFSResult.Owner + "`r`nCreation Date: " + $TFSResult.CreationDate), 'TFS Check in')
            }
            else
            {$null = [System.Windows.Forms.MessageBox]::Show('No changes to check in!', 'TFS Check in')}
        }
    }
    else
    {
        $null = [System.Windows.Forms.MessageBox]::Show('No files to be checked in!', 'TFS Check in')
    }
}

Write-Host
Write-Host -Object 'Updating TFS Workspace' -ForegroundColor Green
Update-TfsWorkspace -Item 'C:\Users\Peter\Documents\Visual Studio 2013\Workspace - Techpath\TechPath Base Class Library\TPBCL' -Recurse
Write-Host
New-Alias -Name 'cf' -Value Checkin-Files
$tfsRoot = $psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Team Foundation Server',$null,$null) 
$tfsRoot.Submenus.Add('Check out current file',  
    {
        $TFSResult = Add-TfsPendingChange -Edit -Item $psISE.CurrentFile.FullPath | Select-Object -Property Version, ChangeType, ServerItem
        if ($result -ne $null)
        {
            $null = [System.Windows.Forms.MessageBox]::Show(('Version: ' + $TFSResult.Version + "`r`nChangeType: " + $TFSResult.ChangeType + "`r`nFile: " + $TFSResult.ServerItem), 'TFS File Checkout')
        }
        else 
        {
            $null = [System.Windows.Forms.MessageBox]::Show('File could not be checked out', 'TFS File Checkout')
        }
    }
, 'Ctrl+Alt+E') | Out-Null
$tfsRoot.SubMenus.Add('Check in Pending Changes',  
    {
        Checkin-Files
    }
, 'Ctrl+Alt+S') | Out-Null

PowerShell: Customising the pipeline inputs

Sometime when using cmdlets there might be cases where you would assume two cmdlets would work together naturally and they just don’t. A good example of this is anything that takes -ComputerName as an input parameter (E.g test-connection) and the Get-ADComputer command.

In this example if you run Get-ADComputer -filter * you get a list of all computers in the current domain, say you now want to run Test-Connection to check machines are online, you might think you could just go:
Get-ADComputer -filter * | test-connection
Unfortunately you will be greeted with a wall of red text, this is because Get-ADComputer does not output an object with a ComputerName property, instead it has either Name or DNSHostName. You might now decide to output it all to a text file and ping them manually but your using PowerShell and there are easy ways do do this on the fly.

Method 1: Create a custom property on the ADComputer object with the name “ComputerName” so the next command receives what it expects:

Get-ADComputer -Filter * | 
   select @{n="ComputerName"; e={$_.DNSHostName}} | 
   Test-Connection

Method 2: Specify the name of the ADComputer object’s property in the next cmdlets input:

Get-ADComputer -Filter * | Test-Connection -ComputerName {$_.DNSHostName}

Using either of the above methods works with any cmdlet that accepts ComputerName as a pipeline input parameter.

Holy Trinity of PowerShell

The three most important cmdlets to remember in PowerShell, in fact the old ones you really need to remember are: Get-Command, Get-Help and Get-Member

Get-Command will give you a list of all installed cmdlets in PowerShell as well as their type, version and source. Additionally it has parameters for -verb or -noun so you can get a full list of all “Get-” cmdlets by typing: get-command -verb get or a list of all cmdlets that deal with services by typing: get-command -noun service

Get-Help is used once you know the command you are after, but you want to know more about how it works or what inputs or output to expect. You can provide some handy parameters to the get-help cmdlet such as -full to show the full help with examples, -showwindow to show the full help output in a searchable windows forms interface or -online to show the full detailed help on the technet website.

Get-Member is by far the cmdlet that I use the most, it gives you any methods and properties the current object in the pipeline has as well as specifying the type of the object your dealing with. This includes any properties that may not be visible by just outputting the object to the screen.

A notable gotcha for this is that if your passing get-member an array of strings (or any collection of any objects) it will give you the properties and methods of a string not an array. To determine if you are dealing with an array use the .gettype() .NET method.

Example:
PS C:\> $a = "Hello World"
PS C:\> $b = "hello", "world" 

PS C:\> $a | Get-Member
TypeName: System.String ..

PS C:\> $b | Get-Member
TypeName: System.String ...

PS C:\> $a.GetType() | select name, basetype
Name        BaseType 
String      System.Object

PS C:\> $b.GetType() | select name, basetype
Name        BaseType 
Object[]    System.Array