Hey, Scripting Guy! Event 6 *Solutions* from Expert Commentators (Beginner and Advanced; the 110-meter hurdles)

Hey, Scripting Guy! Event 6 *Solutions* from Expert Commentators (Beginner and Advanced; the 110-meter hurdles)

  • Comments 5
  • Likes

  

(Note: These solutions were written for Event 6.) 

Beginner Event 6: The 110-meters hurdles

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. 

Guest commentator: Uros Calakovic

Image of guest commentator Uros Calakovic

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.


VBScript solution

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 existsbut 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 Explicit
On Error Resume Next
Dim objShell 'Instance of the WshShell object
Dim strDesktop 'Pointer to desktop special folder
Dim objShortCut 'Used to set properties of the shortcut. Comes from using CreateShortcut
Dim objURL 'Used to set properties of webshortcut.
Dim objNotepad
Dim wshNetwork

set objShell = CreateObject("WScript.Shell")
strDesktop = objShell.SpecialFolders("Desktop")

set objShortCut = objShell.CreateShortcut(strDesktop & "\Shortcut Script.lnk")
objShortCut.TargetPath = WScript.ScriptFullName
objShortCut.WindowStyle = 0
objShortCut.Hotkey = "CTRL+SHIFT+F"
objShortCut.IconLocation = "notepad.exe, 0"
objShortCut.Description = "Shortcut Script"
objShortCut.WorkingDirectory = strDesktop
objShortCut.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 Explicit
On Error Resume Next
Dim objShell ' Instance of the WshShell object
Dim strDesktop ' Pointer to desktop special folder
Dim 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 shortcut

Set objShortcut = objShell.CreateShortcut(strDesktop & _
    "\Shortcut Script.lnk")

objShortcut.TargetPath = WScript.ScriptFullName
objShortcut.WindowStyle = 0
objShortcut.Hotkey = "CTRL+SHIFT+F"
objShortcut.IconLocation = "notepad.exe, 0"
objShortcut.Description = "Shortcut Script"
objShortcut.WorkingDirectory = strDesktop
objShortcut.Save()

' Create the second shortcut

Set objURL = objShell.CreateShortcut(strDesktop & _
    "\The Microsoft Scripting Guys.url")

objURL.TargetPath = "http://www.ScriptingGuys.com"
objURL.Save()

' Create the third shortcut

Set 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 stepscall WshShell.CreateShortcut(), assign its properties, save the shortcutare 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 Explicit

Dim objShell ' Instance of the WshShell object
Dim strDesktop ' Pointer to desktop special folder

Set objShell = CreateObject("WScript.Shell")
strDesktop = objShell.SpecialFolders("Desktop")

AddShortcut strDesktop & "\Shortcut Script.lnk", WScript.ScriptFullName, _
    0, "CTRL+SHIFT+F", "Notepad.exe, 0", "Shortcut Script", strDesktop

AddShortcut strDesktop & "\The Microsoft Scripting Guys.url", _
    "http://www.ScriptingGuys.com", Null, Null, Null, Null, Null


AddShortcut 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.

 

Guest commentator: Jeffery Hicks

Image of guest commentator Jeffery Hicks

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.


Windows PowerShell solution

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:

Image of the finished icons

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"


Advanced Event 6: The 110-meter hurdles

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.

Guest commentator: Steve Lee

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.

VBScript solution

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 = 1
Const defaultTimeout = 4000
Const 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 public
Class TraceSample
          Public avgLatency
          Public destination

          Public Sub Class_Initialize
                   avgLatency = -1
                   destination = "<TraceRT failed>"
          End Sub
End 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 If
End Function

The resulting AdvancedEvent6Solution.vbs script is seen here.

AdvancedEvent6Solution.vbs

Const logname = "100 meter hurdle.txt"
Const ForReading = 1
Const defaultTimeout = 4000
Const numberSamples = 3

Set fso = CreateObject("scripting.filesystemobject")
Set tracelog = fso.OpenTextFile(logname, ForReading)
Set regex = new RegExp
regex.Pattern = "^\s+\d+\s+(\d+|\*).*?(\d+|\*).*?(\d+|\*)\s.*?\s+(.*?)$"

'lazy so just making everything public
Class TraceSample
          Public avgLatency
          Public destination

          Public Sub Class_Initialize
                   avgLatency = -1
                   destination = "<TraceRT failed>"
          End Sub
End Class

Set slowest = new TraceSample

While (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
          Next
Wend

tracelog.Close
WScript.Echo slowest.destination

Function Normalize(latency)
          If latency = "*" Then
                   Normalize = CLng(DefaultTimeout)
          Else
                   Normalize = Clng(latency)
          End If
End Function

 

Guest commentator: Marc van Orsouw

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.


Windows PowerShell solution

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:

Image of the If statement

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:

Number
T1
T2
T3
Host

 

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:

 

Image of the sorted output

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:

Image of the displayed output

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: 

Image of the result of the command

 

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:

 

Image of the 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

 

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • 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,