Leveraging PowerShell Semantics to do More with Less

Leveraging PowerShell Semantics to do More with Less

  • Comments 3

Brad Wilson – the .NET Guy recently posted a set of scripts showing how to drive TortoiseSVN from PowerShell. I thought I would use one of these to highlight how you can leverage the PowerShell semantics to do more with less code.

PowerShell is provides Shell semantics and sometimes allows you to ask for more formal, programming, semantics. Specifically, if you ask for a variable that does not exist – PowerShell does not consider that an error. In the same token, if you ask for a PROPERTY of a variable and that property does not exist, that is not an error either. PowerShell doesn't complain – we give you a null and keep on going.

There are times that this is a pain (e.g. if you just mistype a variable name, that can be hard to track down) so you can turn on STRICT mode by typing:

PS> SET-PSDEBUG –STRICT

This tells PowerShell to throw an exception if you access a variable that does not exist. Over time, we'll offer you more and more options to be more formal in your programming style. That said, there are some huge advantages to the shell semantics so let's explore them for a second. Consider the following script:

tdiff.ps1 (used to diff arbitrary files with TortoiseMerge):

if ($args.Length -lt 2) {

write-host "usage: tdiff <file1> <file2>"

return

}

 

if ((test-path "HKLM:\Software\TortoiseSVN") -eq $false) {

write-host -foregroundColor Red "Error: Could not find TortoiseMerge.exe"

return

}

 

$tortoiseKey = get-itemproperty "HKLM:\Software\TortoiseSVN"

 

if ($tortoiseKey -eq $null) {

write-host -foregroundColor Red "Error: Could not find TortoiseMerge.exe"

return

}

 

$tortoise = $tortoiseKey.TMergePath

 

if ($tortoise -eq $null) {

write-host -foregroundColor Red "Error: Could not find TortoiseMerge.exe"

return

}

 

$commandLine = '/base:"' + $args[0] + '" /mine:"' + $args[1] + '"'

& $tortoise $commandLine

 

Notice this script has 3 validity checks (hightlighted in red) and it generates the same error message for all those checks. It looks for the existence of a key in the registry, it ensures that that key has properties, and then it looks for a specific property. If any of these don't work, it generates an error message and returns. We can do all three checks in a single step below:

tdiff.ps1 (used to diff arbitrary files with TortoiseMerge):

if ($args.Length -lt 2) {

write-host "usage: tdiff <file1> <file2>"

return

}

 

$tortoise = (Get-ItemProperty "HKLM:\Software\TortoiseSVN" –ea SilentlyContinue).TMergePath

 

if ($tortoise -eq $null) {

write-host -foregroundColor Red "Error: Could not find TortoiseMerge.exe"

return

}

 

$commandLine = '/base:"' + $args[0] + '" /mine:"' + $args[1] + '"'

& $tortoise $commandLine

 

The Cmdlet Get-ItemProperty generates an error if you try to access a property that does not exist. We override that behavior with –EA (this is an alias for –ErrorAction). When we specify "–EA SilentlyContinue", the cmdlet does not generate an error and returns a NULL. We put this command in parenthesis which says, "run this command and use its results here". This returns a NULL and then we ask for the "TMergePath" property of a null object – which is NULL of course.

These semantics allow us to significantly reduce the code required to implement this function. It is now easier to read, understand and maintain.

BTW – one last thing. Instead of doing a WRITE-HOST –FOREGROUNDCOLOR RED followed by a RETURN, you can just use a THROW

$tortoise = (Get-ItemProperty "HKLM:\Software\TortoiseSVN" –ea SilentlyContinue).TMergePath

 

if ($tortoise -eq $null) {

Throw "Error: Could not find TortoiseMerge.exe"

}

 

This is simpler, less code, it records the error in $ERROR and it sets the $? Variable so a calling program can switch based upon the success or failure of the operation. Also, recording errors in $ERROR also provides invocation information so it makes it easier to debug where things went wrong. E.g.


PS> cat function:t
$error = "Error: Something went wrong"
if ($args[0] %2)
{ throw $error
}else
{ throw $error
}

PS> t 1
Error: Something went wrong
At C:\ps\t1.ps1:5 char:10
+ { throw <<<< $error
PS> t 2
Error: Something went wrong
At C:\ps\t1.ps1:7 char:10
+ { throw <<<< $error
PS> $error[0].errorrecord.InvocationInfo


MyCommand :
ScriptLineNumber : 7
OffsetInLine : 10
ScriptName : C:\ps\t1.ps1
Line : { throw $error

PositionMessage :
At C:\ps\t1.ps1:7 char:10
+ { throw <<<< $error

InvocationName : throw
PipelineLength : 0
PipelinePosition : 0

PS>

Enjoy!

Jeffrey Snover [MSFT]
Windows PowerShell/MMC Architect
Visit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShell
Visit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

Leave a Comment
  • Please add 2 and 2 and type the answer here:
  • Post
  • I really wish I could get an error when I try to access a property that doesn't exist.  Lots of times typos leave me searching forever to figure out where my script went wrong.

  • > I really wish I could get an error when I try to access a property that doesn't exist.  

    We hear you.  This was included when in my statement:

    "Over time, we'll offer you more and more options to be more formal in your programming style."

    Jeffrey Snover [MSFT]

    Windows PowerShell/MMC Architect

    Visit the Windows PowerShell Team blog at:    http://blogs.msdn.com/PowerShell

    Visit the Windows PowerShell ScriptCenter at:  http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

Page 1 of 1 (3 items)