Learn about Windows PowerShell
(Note: These solutions were written for Event 6.)
In the 110-meter hurdles, you have to overcome a number hurdles as you attempt to make your script run. Debugging skills are useful for this event.
Uros Calakovic is a system and database administrator from Bijeljina, Bosnia and Herzegovina. He is also a moderator for the Official Scripting Guys Forum, and has written articles for the Code Project.
The 110-meter hurdles Beginner Event 6 puts me in just the kind of a situation that I try to avoid when writing a script. The script contains multiple errors hidden by the On Error Resume Next statement at the beginning. If this were a “real life” script, the scripter most likely assembled it in one piece without testing the script once. I avoid these kinds of problems by frequent testing. As a best practice, I run the script each time I add a new piece of executable code. I check for errors during each subsequent running of the script.
When faced with a script like this (mine or somebody else's), my strategy for troubleshooting purposes is similar. If there is an On Error Resume Next statement at the beginning, I remove it, run the script with cscript.exe (from the command line), and resolve the errors one by one as they appear at the command prompt.
For this particular script, I first need to figure out what the script should do from the code itself and the enclosed picture. The script, as the picture suggests, should create three shortcuts on the user’s desktop. The script fails silently, so I follow the usual procedure, and the first error after removing the On Error Resume Next and running the script I get is this:
C:\scripts\beg_6.vbs(18, 1) Microsoft VBScript runtime error: Variable is undefined: 'objShell'
The first statement in the script is Option Explicit, which means that you must declare every variable used in the script. This is considered a good practice, especially if it is a long script, because it prevents you from misspelling a variable name. This is exactly what caused this error: I check the list of declared variables and there is a variable named oShell which is never used in the script, so I change the variable name to objShell. I run the script again and the next error appears:
C:\scripts\beg_6.vbs(28, 1) WshShortcut.Save: Unable to save shortcut "C:\Documents and Settings\Urkec\Desktop \Shortcut Script.lnk".
This error usually occurs when a read-only shortcut with the same path as the one you are trying to create already exists—but not in this case. It takes me a minute to spot what causes the error message. The above path contains an extra space after Desktop\, so I correct the line to:
set objShortCut = objShell.CreateShortcut(strDesktop & "\Shortcut Script.lnk")
I run the script again and two things happen: The first shortcut appears on the desktop, but its icon does not look like the Notepad icon. I open the shortcut’s Properties, click the Change Icon... button, and the dialog offers only one icon for Notepad.exe. Its index is probably zero (0), so I assign the icon to the first shortcut:
objShortCut.IconLocation = "notepad.exe, 0"
At the same time, the first shortcut appears and I receive this error:
C:\scripts\beg_6.vbs(30, 1) Microsoft VBScript runtime error: Variable is undefined: 'wshShell'
The variable named WshShell is used only once in the script to create the WshShell object, which creates the second shortcut. I could add Dim WshShell to the declaration list and the script would work fine, but I decide this is a typo and change the line to:
Set objShell = CreateObject("Wscript.Shell")
This reveals the next error:
C:\scripts\beg_6.vbs(33, 1) Microsoft VBScript runtime error: Object doesn't support this property or method: 'Discription'
At first this looks like just another typo so I change the line to the following code:
objURL.Description = "Scripting Guys"
But, it appears that this is not just another misspelled word, because after running the script again, I get the same error:
C:\scripts\beg_6.vbs(33, 1) Microsoft VBScript runtime error: Object doesn't support this property or method: 'Description'
This means that objURL really doesn't have a member named Description, which is strange because the Description property is successfully used while creating the first shortcut. I temporarily add this line to the script:
WScript.Echo TypeName(objURL)
This checks the objURL type, and the type is:
IWshURLShortcut
I then check the documentation, and the only members for WshURLShortcut are FullName, TargetPath, and Save(), which means that you can't set a description for Internet shortcuts and the line must be deleted. Also, just as with the first shortcut, the second shortcut must be saved in order to appear on the desktop, so I add this line:
objURL.Save()
After this change, the first two shortcuts are created, and the next error shown is:
C:\scripts\beg_6.vbs(35, 1) Microsoft VBScript runtime error: Variable is undefined: 'wshNetwork'
This variable holds the WScript.Network object never used in the script, but I still add this line to the declaration section of the script:
Dim wshNetwork
After that I run the script and the next error is:
C:\scripts\beg_6.vbs(38, 1) WshShell.CreateShortcut: The shortcut pathname must end with .lnk or .url.
This is easy, and I change the line to:
set objShortCut = objShell.CreateShortcut(strDesktop & "\notepad.lnk")
The last error is:
C:\scripts\beg_6.vbs(40, 1) Microsoft VBScript runtime error: Invalid procedure call or argument
The documentation says WshShortcut.TargetPath must be the path to the executable, but I just change notpad.exe to notepad.exe and the script finally seems to be error-free. Three working shortcuts are now created on my desktop.
This is what the script looks like after the above session:
'================================================================='' VBScript: AUTHOR: Ed Wilson , msft, 5/8/2009'' NAME: Beg_6.vbs'' problem script for Beginner event 6.' Summer Scripting Games 2009'=================================================================Option ExplicitOn Error Resume NextDim objShell 'Instance of the WshShell objectDim strDesktop 'Pointer to desktop special folderDim objShortCut 'Used to set properties of the shortcut. Comes from using CreateShortcutDim objURL 'Used to set properties of webshortcut. Dim objNotepadDim wshNetworkset objShell = CreateObject("WScript.Shell")strDesktop = objShell.SpecialFolders("Desktop")set objShortCut = objShell.CreateShortcut(strDesktop & "\Shortcut Script.lnk")objShortCut.TargetPath = WScript.ScriptFullNameobjShortCut.WindowStyle = 0objShortCut.Hotkey = "CTRL+SHIFT+F"objShortCut.IconLocation = "notepad.exe, 0"objShortCut.Description = "Shortcut Script"objShortCut.WorkingDirectory = strDesktopobjShortCut.Save()Set objShell = CreateObject("Wscript.Shell")set objURL = objShell.CreateShortcut(strDesktop & "\The Microsoft Scripting Guys.url")objURL.TargetPath = "http://www.ScriptingGuys.com"objURL.Save()Set wshNetwork = CreateObject("WScript.Network")set objShortCut = objShell.CreateShortcut(strDesktop & "\notepad.lnk")objShortCut.TargetPath = "notepad.exe"objShortCut.IconLocation = "notepad.exe, 0"objShortCut.description = "notepad"objShortCut.Save
The script now works fine, but there is still some room for improvement. I can make some changes without affecting the script’s functionality. First, I will make some cosmetic changes. I capitalize all VBScript keywords, add a couple of empty lines, and so on.
Next, I remove all unnecessary parts. The objNotepad variable is declared, but is never used in the script. WshNetwork is declared and a WScript.Network instance is created, but you don't need this object to create desktop shortcuts. And a WScript.Shell instance is created twice.
The script now looks like this:'=========================================================================='' VBScript: AUTHOR: Ed Wilson , msft, 5/8/2009'' NAME: Beg_6.vbs'' problem script for Beginner event 6.' Summer Scripting Games 2009' This script does not work. It needs to be fixed. '==========================================================================Option ExplicitOn Error Resume NextDim objShell ' Instance of the WshShell objectDim strDesktop ' Pointer to desktop special folderDim objShortcut ' Used to set properties of the shortcut. Dim objURL ' Used to set properties of webshortcut. Set objShell = CreateObject("WScript.Shell")strDesktop = objShell.SpecialFolders("Desktop")' Create the first shortcutSet objShortcut = objShell.CreateShortcut(strDesktop & _ "\Shortcut Script.lnk")objShortcut.TargetPath = WScript.ScriptFullNameobjShortcut.WindowStyle = 0objShortcut.Hotkey = "CTRL+SHIFT+F"objShortcut.IconLocation = "notepad.exe, 0"objShortcut.Description = "Shortcut Script"objShortcut.WorkingDirectory = strDesktopobjShortcut.Save()' Create the second shortcutSet objURL = objShell.CreateShortcut(strDesktop & _ "\The Microsoft Scripting Guys.url")objURL.TargetPath = "http://www.ScriptingGuys.com"objURL.Save()' Create the third shortcutSet objShortcut = objShell.CreateShortcut(strDesktop & _ "\notepad.lnk")objShortcut.TargetPath = "notepad.exe"objShortcut.IconLocation = "notepad.exe, 0"objShortcut.description = "notepad"objShortcut.Save()
A script like this is fine if you don't have to create shortcuts often. But if you do, constantly repeating the same task can quickly become tedious. If you look at the script, you can see that the same steps—call WshShell.CreateShortcut(), assign its properties, save the shortcut—are performed for each shortcut. This type of code is a good candidate for a function (or a subroutine). By using functions for repetitious tasks, you can save yourself a lot of typing. The other advantage is, if you put a piece of code in a function and test it thoroughly, you can use the function as a black box and don’t have to think about the code logic again. Here is how the script could look like when a subroutine is used for creating a shortcut:
'=========================================================================='' VBScript: AUTHOR: Ed Wilson , msft, 5/8/2009'' NAME: Beg_6.vbs'' problem script for Beginner event 6.' Summer Scripting Games 2009' ' Uros Calakovic, 5/16/2009' use a function to avoid repeating the task' '==========================================================================Option ExplicitDim objShell ' Instance of the WshShell objectDim strDesktop ' Pointer to desktop special folderSet objShell = CreateObject("WScript.Shell")strDesktop = objShell.SpecialFolders("Desktop")AddShortcut strDesktop & "\Shortcut Script.lnk", WScript.ScriptFullName, _ 0, "CTRL+SHIFT+F", "Notepad.exe, 0", "Shortcut Script", strDesktopAddShortcut strDesktop & "\The Microsoft Scripting Guys.url", _ "http://www.ScriptingGuys.com", Null, Null, Null, Null, NullAddShortcut strDesktop & "\notepad.lnk", "notepad.exe", 651, _ Null, "notepad.exe, 0", "notepad", Null'=========================================================================='' Creates a shortcut'' Input parameters:' strShortcutPath - path to the shortcut including its name and extension' strTargetPath - the path to the shortcut's executable' intWindowStyle - the window style for the shortcut' strHotkey - the key combination to the shortcut' strIconLocation - the icon location for the shortcut' strDescription - shortcut's description' strWorkingDirectory - working directory for the shortcut ''==========================================================================Sub AddShortcut(strShortcutPath, strTargetPath, intWindowStyle, _ strHotkey, strIconLocation, strDescription, strWorkingDirectory) Dim objWshShell Dim objShortcut Set objWshShell = CreateObject("WScript.Shell") Set objShortcut = objWshShell.CreateShortcut(strShortcutPath) objShortcut.TargetPath = strTargetPath If TypeName(objShortcut) = "IWshShortcut" Then If Not IsNull(intWindowStyle) Then objShortcut.WindowStyle = intWindowstyle End if If Not IsNull(strHotKey) Then objShortcut.Hotkey = strHotkey End If If Not IsNull(strIconLocation) Then objShortcut.IconLocation = strIconLocation End If If Not IsNull(strDescription) Then objShortcut.Description = strDescription End If If Not IsNull(strWorkingDirectory) Then objShortcut.WorkingDirectory = strWorkingDirectory End If End If objShortcut.Save()End Sub
When creating a shortcut, you don't have to assign all of the WshShortcut properties. In the above function, the WshShortcut properties are passed as input parameters. VBscript doesn't support optional parameters, so I had to simulate this by using the Null keyword. If Null is passed for a parameter, the property doesn't get assigned. I also decided to use the same procedure for creating both types of shortcuts, which forced me to test for the object type created by WshShell.CreateShortcut() function and assign the properties accordingly. The procedure could be further refined by checking if strTargetPath and strWorkingDirectory exist, checking if intWindowStyle falls within the allowed range from the WshWindowStyle enumeration, etc.
Jeffery Hicks is an MCSE, MCSA, MCT, and Microsoft PowerShell MVP. He is a scripting guru for SAPIEN Technologies. Jeff is a 17-year IT veteran. He has co-authored and authored several books, courseware, and training videos on administrative scripting and automation. His latest book is Managing Active Directory with Windows PowerShell: TFM (SAPIEN Press 2008). Jeff also writes the Mr. Roboto column for REDMOND Magazine, Professor PowerShell for MCPMag.com and Practical PowerShell for RealTime Publishers. Visit http://blog.sapien.com for Jeff’s latest work and you can follow Jeff on Twitter at http://www.twitter.com/JeffHicks.
The first troubleshooting step is to see which errors are occurring so I first comment out this line:
$ErrorActionPreference = "SilentlyContinue"
The script includes a line to use the interactive debugging cmdlet, which I could have modified:
Set-PSDebug -Strict -Step -Trace 1
Personally, I find this to be more complicated to understand, especially for Windows PowerShell beginners. I prefer to insert test commands into my script to check variables and objects while the script is running. You can either comment out or remove this line from your script. Now let’s see which errors I have to deal with.
Running the script results in an error about a missing “)” on line 33. Loading the script into a script editor like PrimalScript that displays the line numbers makes it easier troubleshoot. Line 33 doesn’t require any parentheses. In Windows PowerShell, sometimes the code causing the error happens before the line that generates the error. In looking at the previous line, the “&” character is not used to concatenate as it is in VBScript. My scripting editor includes syntax checking and also indicates something is wrong with this line. I change the “&” to a “+” and run the script again.
Now I get an error that the variable $objShell cannot be retrieved because it was not set on line 20. Windows PowerShell is trying to use an object called $objShell. Where did that come from? Working backward from this point, I can’t find any Windows PowerShell commands that create this object. From my VBScript experience, I know that the CreateShortcut method is part of the WshShell object. Reading through the code I find:
$oShell = New-Object -ComObject ("WScript.Shell")
This is not the same object. So I change the line to:
$objShell = New-Object -ComObject ("WScript.Shell")
And try again. Remember to only change one thing at a time and then test. There are still several problems, but I'll tackle them in order one at a time.
The first problem is that Windows PowerShell is displaying information about a Save method. If you call an object's method without using (), this is the type of thing you'll see. I look for the first Windows PowerShell line that is calling the Save method and find:
$objShortCut.Save
It should be:
$objShortCut.Save()
The next error is about a Discription property that can’t be found:
$objURL.Discription = "Scripting Guys"
More than likely, that is not the right property name. It could be Description. But I must confirm my assumption. I can pipe the $objURL object to Get-Member with a few lines of code as seen here:
$objShell = New-Object -ComObject ("WScript.Shell")$strDesktop = $objShell.SpecialFolders.item("Desktop")$objURL = $objShell.CreateShortcut($strDesktop + "\The Microsoft Scripting Guys.url")$objURL | get-member
Looking at the output I discover there is no Description property. Not only is it a typo, the property doesn't even exist (even if it was spelled correctly)! I will comment this line out for now and run the script again.
Only one error apparently left, that the shortcut name must end in .lnk or .url. Here’s the offending line:
$objShortCut = $objShell.CreateShortcut($strDesktop + "\notepad.link")
Because this is a file shortcut link, I’ll change the extension to .lnk.
The script runs with errors and I get the desktop shortcuts, but before I test them, I notice that the icon for the script shortcut is not very pretty. The code looks like it is trying to use the same icon as I'm using for the Notepad.exe shortcut. But the IconLocation property for the script shortcut is set to:
$objShortCut.IconLocation = "notepad.exe, 2"
I'll change the icon index to 0 and try again. And…bingo!
Now to test the shortcuts to make sure they work. I first try the Notepad shortcut and get an error message that Windows is searching for notpad.exe. I look at the code that created the shortcut and see the problem:
$objShortCut.TargetPath = "notpad.exe"
That’s an easy fix. I'll delete the desktop shortcuts and test again. No errors and the Notepad link now works. Testing the shortcut link, which should open the script I'm debugging, fails. The link opens up My Computer so I suspect a path problem. This is the likely culprit:
$objShortCut.TargetPath = $MyInvocation.ScriptName
The script is trying to use the ScriptName property of the intrinsic $Myinvocation object. If I've never seen this variable before, I'll want to look at it with Get-Member and see what properties it has. I'll insert these lines temporarily at the beginning of my script:
$MyInvocation | select *$MyInvocation | get-member
When I run the script, I can see that the ScriptName property is not defined. But the MyCommand property has the scriptname. By looking at the Get-Member information, I can see that this property is an object:
MyCommand Property System.Management.Automation.CommandInfo MyCommand
I wonder what properties this object has. I'll modify the test code:
$MyInvocation.Mycommand | select *
The results are illuminating. It appears that the Path property has the information I need, so I'll comment out my test code and modify the appropriate line of code:
$objShortCut.TargetPath = $MyInvocation.MyCommand.Path
I'll delete all the desktop shortcuts and re-run the script. Now the shortcut link launches my script in Notepad, which is the default behavior for a Windows PowerShell script. I test the Microsoft Scripting Guy’s link and it works. You can see the finished icons here:
All that remains is to clean up the script a bit. I'll delete any test lines of code, although commenting them out is probably better so I can re-use them for future troubleshooting. Even though the script now works as expected, I can spot another typo:
New-Variable -Name obShell #instance of the wshSHell object
The variable we are using is objShell. I can change this line, or I can simply remove the New-Variable lines. Windows PowerShell does not require you to declare variables in advance nor is there a Windows PowerShell equivalent of VBScript’s Option Explicit. I also see some lines of code that create objects that are never used:
$wshShell = New-Object -ComObject wscript.shell
and
$wshNetwork = New-Object -ComObject wscript.network
Finally, I'll make a few changes so that this script is more like Windows PowerShell. We don't need to concatenate strings to create a filepath. Instead, using the Join-Path cmdlet is a better approach:
$shortcut=Join-Path -path $strDesktop -childpath "Shortcut Script.lnk"$objShortCut = $objShell.CreateShortcut($shortcut)
We now have a functional script, BeginnerEvent6Solution1.ps1. Here it is:
BeginnerEvent6Solution1.ps1
#==========================================================================## PowerShell: AUTHOR: Jeffery Hicks, SAPIEN Technologies, 5/13/2009## NAME: Beg_6_Corrected.ps1## COMMENT: Key concepts are listed below:#1. Uses wscript.shell to create Three shortcuts on the desktop. The first is shortcut#2. to this actual script. It uses the scriptfullName property to assign the path.#3. The second is a simple website URL shortcut, the third one is a shortcut To#4. Notepad.#==========================================================================$objShell = New-Object -ComObject ("WScript.Shell")$strDesktop = $objShell.SpecialFolders.item("Desktop")$shortcut=Join-Path -Path $strDesktop -childpath "Shortcut Script.lnk"$objShortCut = $objShell.CreateShortcut($shortcut)$objShortCut.TargetPath = $MyInvocation.MyCommand.Path$objShortCut.WindowStyle = 0$objShortCut.Hotkey = "CTRL+SHIFT+F"$objShortCut.IconLocation = "notepad.exe, 0"$objShortCut.Description = "Shortcut Script"$objShortCut.WorkingDirectory = $strDesktop$objShortCut.Save()$shortcut=Join-Path -Path $strDesktop -childpath "The Microsoft Scripting Guys.url"$objURL = $objShell.CreateShortcut($shortcut)$objURL.TargetPath = "http://www.ScriptingGuys.com"$objURL.Save()$shortcut=Join-Path -Path $strDesktop -childpath "Notepad.lnk"$objShortCut = $objShell.CreateShortcut($shortcut)$objShortCut.TargetPath = "notepad.exe"$objShortCut.IconLocation = "notepad.exe, 0"$objShortCut.description = "Notepad"$objShortCut.Save()
There is one problem with the BeginnerEvent6Solution1.ps1 script; it still feels too much like a translated VBScript and not like a Windows PowerShell script. The BeginnerEvent6Solution1.ps1 script is creating several shortcuts with essentially the same object and the same method, which means code duplication. Whenever I see code that essentially does the same thing, I want to create a function. Here is one way to create a New-ShortCut function:
Function New-Shortcut { Param([string]$path=$(Throw "You must enter a path for the shortcut"), [string]$targetpath=$(Throw "You must enter a target path for the shortcut"), [string]$WorkingDirectory=$env:Windir, [int]$WindowStyle=0, [string]$description, [string]$HotKey, [string]$iconLocation ) $objShell = New-Object -ComObject ("WScript.Shell") $objShortCut = $objShell.CreateShortcut($path) $objShortCut.TargetPath = $targetpath #only .lnk shortcuts have these properties if ($shortcut -match ".lnk") { $objShortCut.WindowStyle = $WindowStyle $objShortCut.Hotkey = $HotKey $objShortCut.IconLocation = $iconLocation $objShortCut.Description = $description $objShortCut.WorkingDirectory = $WorkingDirectory } $objShortCut.Save()}
I can call this function as many times as I want with a one-line command:
$shortcut=Join-Path -Path $strDesktop -childpath "Shortcut Script.lnk"New-Shortcut -path $shortcut -targetpath $MyInvocation.MyCommand.Path -hotkey "CTRL+SHIFT+F" -iconlocation "notepad.exe, 2" -description "Shortcut Script" -workingDirectory $strDesktop
I've included the BeginnerEvent6Solution2.ps1 script that demonstrates this Windows PowerShell approach. This is shown here:
BeginnerEvent6Solution2.ps1
#==========================================================================## PowerShell: AUTHOR: Jeffery Hicks, SAPIEN Technologies, 5/13/2009## NAME: Beg_6_Corrected_B.ps1## COMMENT: Key concepts are listed below:#1. Uses wscript.shell to create Three shortcuts on the desktop. The first is shortcut#2. to this actual script. It uses the scriptfullName property to assign the path.#3. The second is a simple website URL shortcut, the third one is a shortcut To#4. Notepad.#==========================================================================Function New-Shortcut { Param([string]$path=$(Throw "You must enter a path for the shortcut"), [string]$targetpath=$(Throw "You must enter a target path for the shortcut"), [string]$WorkingDirectory=$env:Windir, [int]$WindowStyle=0, [string]$description, [string]$HotKey, [string]$iconLocation ) $objShell = New-Object -ComObject ("WScript.Shell") $objShortCut = $objShell.CreateShortcut($path) $objShortCut.TargetPath = $targetpath #only .lnk shortcuts have these properties if ($shortcut -match ".lnk") { $objShortCut.WindowStyle = $WindowStyle $objShortCut.Hotkey = $HotKey $objShortCut.IconLocation = $iconLocation $objShortCut.Description = $description $objShortCut.WorkingDirectory = $WorkingDirectory } $objShortCut.Save()}$objShell = New-Object -ComObject ("WScript.Shell")$strDesktop = $objShell.SpecialFolders.item("Desktop")$shortcut=Join-Path -Path $strDesktop -childpath "Shortcut Script.lnk"New-Shortcut -path $shortcut -targetpath $MyInvocation.MyCommand.Path `-hotkey "CTRL+SHIFT+F" -iconlocation "notepad.exe, 0" `-description "Shortcut Script" -workingDirectory $strDesktop$shortcut=Join-Path -Path $strDesktop -childpath "Notepad.lnk"New-Shortcut -path $shortcut -targetpath "Notepad.exe" `-iconLocation "notepad.exe,0" -description "Notepad"$shortcut=Join-Path -Path $strDesktop -childpath "The Microsoft Scripting Guys.url"New-Shortcut -path $shortcut -targetpath "http://www.ScriptingGuys.com"
In the 110-meter hurdles event, you will be asked to find out where a particular packet is being held up as you analyze diagnostic information to determine which hop is the fastest.
Steve Lee is a senior test manager on the Windows Management Platform Team at Microsoft. He has worked on WMI since it was first an installable package on Windows NT 4.0. Steve was also an initial member of the team that drove WS-Management to become a ratified standard. He and that team also implemented WS-Management as WinRM on Windows. Steve's team maintains a blog on MSDN that covers all topics related to management using Windows technologies. This blog has an emphasis on WMI, WSMan, and BITS.
The most difficult part of this problem is the ambiguity around finding where the “slowdown is occurring.” Because TraceRt takes three samples, should we just take the longest single sample? This may not be correct because a network blip may produce one long ping while the other two pings are really fast. To keep things simple, I’ll just use the average of the three samples.
I like to avoid “magic numbers” as much as possible, so I define my constants up front:
Const logname = "100 meter hurdle.txt"Const ForReading = 1Const defaultTimeout = 4000Const numberSamples = 3
When I see a text parsing problem, I immediately think of regular expressions. I admit that reading a regular expression is not the easiest thing, but it’s probably easier to read than potentially a bunch of Split() and InStr() calls. I wanted to capture each of the three trace sample as well as the destination separately and retrieve them easily later, so I used the grouping syntax:
regex.Pattern = "^\s+\d+\s+(\d+|\*).*?(\d+|\*).*?(\d+|\*)\s.*?\s+(.*?)$"
I knew I had to keep track of the slowest trace I had seen and for each trace I had to keep track of the average latency and the destination. I created a class to store this information, which makes the script easier to read. If the class was intended to be used in other scripts, I would have abstracted it with private fields and public assessors, but that seemed overkill for this script:
'lazy so just making everything publicClass TraceSample Public avgLatency Public destination Public Sub Class_Initialize avgLatency = -1 destination = "<TraceRT failed>" End SubEnd Class
In the main loop, we just go through each sample, compute the average latency, see if the whole thing timed out (which we throw away because we don’t know the destination) and keep track of the slowest we’ve seen so far. To handle the case of the asterisk (*) (meaning the ping timed out), I wrote a simple function to handle this:
Function Normalize(latency) If latency = "*" Then Normalize = CLng(DefaultTimeout) Else Normalize = Clng(latency) End IfEnd Function
The resulting AdvancedEvent6Solution.vbs script is seen here.
AdvancedEvent6Solution.vbs
Const logname = "100 meter hurdle.txt"Const ForReading = 1Const defaultTimeout = 4000Const numberSamples = 3Set fso = CreateObject("scripting.filesystemobject")Set tracelog = fso.OpenTextFile(logname, ForReading)Set regex = new RegExpregex.Pattern = "^\s+\d+\s+(\d+|\*).*?(\d+|\*).*?(\d+|\*)\s.*?\s+(.*?)$"'lazy so just making everything publicClass TraceSample Public avgLatency Public destination Public Sub Class_Initialize avgLatency = -1 destination = "<TraceRT failed>" End SubEnd ClassSet slowest = new TraceSampleWhile (not tracelog.AtEndOfStream) line = tracelog.ReadLine Set matches = regex.Execute(line) For Each match in matches Set submatches = Match.Submatches totalLatency = 0 For i = 0 to 2 totalLatency = totalLatency + Normalize(submatches(i)) Next If (totalLatency = defaultTimeOut*numberSamples) Then Exit For 'request timed out End If Set sample = new TraceSample sample.avgLatency = CDbl(totalLatency)/numberSamples sample.destination = submatches(3) 'ok, one magic number If (sample.avgLatency > slowest.avgLatency) Then Set slowest = sample End If NextWendtracelog.CloseWScript.Echo slowest.destinationFunction Normalize(latency) If latency = "*" Then Normalize = CLng(DefaultTimeout) Else Normalize = Clng(latency) End IfEnd Function
Marc van Orsouw is a senior consultant at Winworkers Schweiz. Marc is also a Microsoft PowerShell MVP and maintains the PowerShell Guy blog where you can find his PowerTab function for Windows PowerShell.
For this event, we are asked to parse a provided log file created by the Tracert command so that we can determine things such as:
· Which hop is the fastest?
· Where along the interconnected network systems is the slowdown is occurring?
The main goal for this event is to parse the text data output by the Tracert command into a more “PoSHy” (object-based) format so that it becomes easy to work with the data in Windows PowerShell and to answer the questions above. Additionally, I decided to go a bit further on this event and create a Trace-Route cmdlet that could do a live trace as well as parsing a log file.
After creating the Trace-Route function, I also created a FormatData file to tune the default output format for the Trace-Route function. Additional information about that is covered later on.
Next, I use an If statement to check if the –log switched parameter is present when the script is launched. If it is present, the function reads the log file, or it will start Tracert with the host name that it was given.
Because an If statement will break the pipeline, I prevent this by putting the If statement into a scriptblock and invoke that. I do this by wrapping the If statement with a dot and a pair of curly brackets ({}) as seen here:
I do not want to use a variable here because I want to maintain the Streaming behavior when we are not reading a log file. We will talk about that later.
I break each line on two or more spaces by using a regex split (for Windows PowerShell 2.0, we can use the –split operator). This is seen here:
$row = [regex]::split($_.trim(),'\s{2,}')
From this row, I create a custom object for each row that starts with a number, so the extra output that is output by the Tracert command is ignored. Also note the use of the trim() method that will remove the trailing spaces before the number.
After that I convert each line into an object with the following properties:
NumberT1T2T3Host
And when I have at least one successful reply, I will add another property to $measurement, with the average time of all successful replies:
Time
Objects that do not have an average time, such as timeouts, will not get a Time property at all. As a result, they will always be at the end of the list when sorting. This behavior is exactly what we want. Now we have the Tracert output structured in an object-based format. We can easily use Windows PowerShell to retrieve the information we need. The sorted output is seen here:
The Windows PowerShell default formatter will show the data in a list format. But we can overrule this behavior by using the Format-Table cmdlet. We use the –autoSize parameter to tighten up the displayed output. This is shown here:
Because the data is structured, we can sort the data and filter based upon property values. This makes it easy to get the information we want from the Tracert log. It also allows us to treat the Tracert log as we would any other Windows PowerShell object. To sort the Tracert log data in a descending manner, based on the Time property, we would use a command such as this:
PowerShell c:\Users\morsouw> trace-rout –log c:\scripts\tracert.txt | Sort time –desc | Select –first 1
The results of this command are shown here:
But there was still one thing bugging me. That was the default format of the output of a list format because it has more than four properties in the object. But with our object, the handiest format for the output is a table view. This requires us to explicitly format the data as a table and specify the -autoSize parameter each time we want to view the data. Certainly, we can use aliases for the cmdlets and achieve a tight syntax, but still it is a lot of unnecessary work.
An additional concern arises if we use the Trace-Route function to directly trace a route to host. When the output is streaming, as in the original Tracert command, we cannot use the Format-Table cmdlet with the –autosize parameter because this will stall the pipeline until all results are in. This is due to the fact that the –autosize parameter needs all the data to calculate the maximum size that each column can occupy.
I did not like this, but luckily this is configurable in Windows PowerShell by using a custom FormatData file. So I created one, called mow.TracertData.ps1xml:
<?xml version="1.0" encoding="utf-8" ?><Configuration> <ViewDefinitions> <View> <Name>Mow.TracertData</Name> <ViewSelectedBy> <TypeName>Mow.TracertData</TypeName> </ViewSelectedBy>
<TableControl> <TableHeaders> <TableColumnHeader> <Width>7</Width> </TableColumnHeader> <TableColumnHeader> <Width>5</Width> </TableColumnHeader> <TableColumnHeader> <Width>5</Width> </TableColumnHeader> <TableColumnHeader> <Width>5</Width> </TableColumnHeader> <TableColumnHeader> <Width>5</Width> </TableColumnHeader> <TableColumnHeader/> </TableHeaders> <TableRowEntries> <TableRowEntry> <TableColumnItems> <TableColumnItem> <PropertyName>Number</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>T1</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>T2</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>T3</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Time</PropertyName> </TableColumnItem> <TableColumnItem> <PropertyName>Host</PropertyName> </TableColumnItem> </TableColumnItems> </TableRowEntry> </TableRowEntries> </TableControl> </View> </ViewDefinitions></Configuration>
This was a multi-step process, which is listed here:
· Give our object a name so we can recognize it.
· Add the type name: $measurement.psobject.TypeNames.Add('Mow.TracertData').
· Create a FormatData file for it.
· Load that FormatData file.
· Update-FormatData c:\scripts\mow.TracertData.ps1xml
Now the output will be formatted as a nice table by default (with explicit column width) more like the original Tracert command, and when used interactively we will get the same streaming behavior:
Note When we sort, we still have to wait until all results are in.
As mentioned in the event description, parsing text output or log files is a routine task for IT pros. I hope this is a useful example of how to tackle these kinds of tasks in Windows PowerShell in your daily job. Here is the full AdvancedEvent6Solution.ps1 script.
AdvancedEvent6Solution.ps1
function Trace-Route ($hostname,$log) { # Create collection of measurement objects for each hop .{ if ($log) { # read the tracert log get-content $log }else{ # execute Tracert command tracert $hostname } } | foreach { # split on a double space $row = [regex]::split($_.trim(),'\s{2,}') # raise error when host could not be resolved if ($row[0] -match 'Unable') {throw $_} # if line starts with a number, process line (removes nonrelevant lines from input) if ($row[0] -match '^\d') { # create PsCustom Object for each measurement $measurement = new-object PSObject # Add custom typename to object for use by formatdata $measurement.psobject.TypeNames.Add('Mow.TracertData') add-member -inputObject $measurement -membertype noteproperty -name Number -value $row[0] # remove MS and compensate for <1 measurements set them to 0 to keep times numeric for sorting add-member -inputObject $measurement -membertype noteproperty -name T1 `-value $row[1].split()[0].replace('<1','0') add-member -inputObject $measurement -membertype noteproperty -name T2 `-value $row[2].split()[0].replace('<1','0') add-member -inputObject $measurement -membertype noteproperty -name T3 `-value $row[3].split()[0].replace('<1','0') # create average time using each successfull reply $time,$counter = 0 if ($measurement.t1 -ne '*'){$counter++;$time += [int]$measurement.t1} if ($measurement.t2 -ne '*'){$counter++;$time += [int]$measurement.t2} if ($measurement.t3 -ne '*'){$counter++;$time += [int]$measurement.t3} # if we have at least one succesful reply, add time member for average time to measurement object if($counter -gt 0 ) {add-member -in $measurement -membertype noteproperty -name Time -value ([int]($time / $counter))} # Add hostname and output measurement object add-member -inputObject $measurement -membertype noteproperty -name Host -value $row[4] $measurement } }} # update default output format with formatData file Update-FormatData c:\scripts\mow.TracertData.ps1xml
We thank Uros, Jeffery, Steve, and Marc for your awesome write-ups today. The tips, techniques, and detailed commentary are much appreciated. It is an honor to be associated with masters of the script-writing craft! This was a great way to begin the second week of the 2009 Summer Scripting Games. Join us tomorrow as we usher in another group of scripting experts who will guide us through the intricacies of Event 7, the discus throw. Keep your submissions coming in, and remember, for questions about events you can go to the special Summer Scripting Games forum, and for all the latest information you can follow us on Twitter. Until tomorrow, keep on scripting!
Ed Wilson and Craig Liebendorfer, Scripting Guys
All the Scripting Games links in one location! Let the learning begin. (We will update this page every
My solution for Advanced event 6 of the Summer Scripting Games is posted at the Script Center : Hey,