Summary: Ed Wilson, Microsoft Scripting Guy, talks about using a cmdlet to trace the execution of a Windows PowerShell script.
Hey, Scripting Guy! I am having a problem with a script. It does not generate any errors, but dude, it does not seem to work either. Can you help me debug it?
—DR
Hello DR,
Microsoft Scripting Guy, Ed Wilson, is here. When I see a script that doesn’t work, I think, "Cool…it is easy to troubleshoot." Often this is the case because the error message helps locate the source of the error. But if a script simply doesn’t work, it can be more difficult to troubleshoot. If the error is a logic error, it can be very difficult to troubleshoot.
Most of the time, examining the values of variables does not solve the problem because the code itself works fine. The problem often lies in what are called "the business rules" of the script. These are decisions the code makes that have nothing to do with the correct operation of, for example, a switch statement. At times, it may appear that the switch statement is not working correctly because the wrong value is displayed at the end of the code. But quite often the business rules themselves are causing the problem.
The problem with logic errors
For a simple example of a logic error, consider the function called My-function that is shown here:
My-Function.ps1
Function my-function
{
Param(
[int]$a,
[int]$b)
"$a plus $b equals four"
}
The My-function function accepts two command-line parameters: a and b. It then combines the two values and outputs a string that states the value is four. The tester performs four different tests, and each time the function performs as expected. These tests and the associated output are shown here:
PS C:\> C:\fso\my-function.ps1
PS C:\> my-function -a 2 -b 2
2 plus 2 equals four
PS C:\> my-function -a 1 -b 3
1 plus 3 equals four
PS C:\> my-function -a 0 -b 4
0 plus 4 equals four
PS C:\> my-function -a 3 -b 1
3 plus 1 equals four
When the function goes into production, however, users begin to complain. Most of the time, the function displays incorrect output. However, the users also report that no errors are generated when the function runs.
What is the best way to solve the logic problem? Simply adding a couple of Write-Debug commands to display the values of the variables a and b will more than likely not lead to the correct solution. A better way is to step through the code one line at a time and examine the associated output. The easy way to do this is to use the Set-PSDebug cmdlet.
Using the Set-PSDebug cmdlet
The Set-PSDebug cmdlet has been around since Windows PowerShell 1.0. This does not mean it is a neglected feature, but rather, that it does what it needs to do. The Set-PSDebug cmdlet is not designed to do heavy debugging; it is a lightweight tool that is useful when you want to produce a quick trace or rapidly step through a script.
For performing basic debugging quickly and easily, you cannot beat the combination of features that are available. There are three things you can do with the Set-PSDebug cmdlet:
- Trace script execution in an automated fashion
- Step through the script interactively
- Enable strict mode to force good Windows PowerShell coding practices.
Today, I'll begin to examine tracing the script...
Tracing the script
One of the simplest ways to debug a script is to turn on script-level tracing. When you turn on script-level tracing, each command that is executed is displayed in the Windows PowerShell console. By watching the commands as they are displayed, you can determine if a line of code in your script executes or if it is being skipped.
To enable script tracing, you use the Set-PSDebug cmdlet and specify one of three levels for the -trace parameter:
Trace level | Meaning |
0 | Turns off script tracing. |
1 | Traces each line of the script as it is executed. Lines in the script that are not executed are not traced. Does not display variable assignments, function calls, or external scripts. |
2 | Traces each line of the script as it is executed. Lines in the script that are not executed are not traced. Displays variable assignments, function calls, and external scripts. |
To understand the process of tracing a script and the differences between the trace levels, examine the CreateRegistryKey.ps1 script. It contains a single function called Add-RegistryValue. In the Add-RegistryValue function, the Test-Path cmdlet is used to determine if the registry key exists.
If the registry key exists, a property value is set. If the registry key does not exist, the registry key is created and a property value is set. The Add-RegistryValue function is called when the script executes. The complete CreateRegistryKey.ps1 script is shown here:
CreateRegistryKey.ps1
Function Add-RegistryValue
{
Param ($key,$value)
$scriptRoot = "HKCU:\software\ForScripting"
if(-not (Test-Path -path $scriptRoot))
{
New-Item -Path HKCU:\Software\ForScripting | Out-Null
New-ItemProperty -Path $scriptRoot -Name $key -Value $value `
-PropertyType String | Out-Null
}
Else
{
Set-ItemProperty -Path $scriptRoot -Name $key -Value $value | `
Out-Null
}
} #end function Add-RegistryValue
# *** Entry Point to Script ***
Add-RegistryValue -key forscripting -value test
Working with trace level 1
When the trace level is set to 1, each line in the script that executes is displayed to the Windows PowerShell console. To set the trace level to 1, you use the Set-PSDebugcmdlet and assign a value of 1 to the -trace parameter.
When the trace level has been set, it applies to everything that is typed in the Windows PowerShell console. If you run an interactive command, a cmdlet, or a script, it will be traced. When the CreateRegistryKey.ps1 script is run and there is no registry key present, the first debug line in the command displays the path to the script that is being executed.
Because Windows PowerShell parses from the top down, the next line that is executed is the line that creates the Add-RegistryValue function. This command is on line 7 of the script because the actual script that executed contains six lines that are commented out.
After the function is created, the next line of the script that executes is line 30. Line 30 of the CreateRegistryKey.ps1 script follows the comment that points to the entry point of the script (this is the last line), and it calls the Add-RegistryValue function by passing two values for the -key and -value parameters. This is shown here:
PS C:\> Set-PSDebug -Trace 1
PS C:\> C:\fso\CreateRegistryKey.ps1
DEBUG: 1+ >>>> C:\fso\CreateRegistryKey.ps1
DEBUG: 30+ >>>> Add-RegistryValue -key forscripting -value test
After control of script execution is inside the Add-RegistryValue function, the HKCU:\software\ForScripting string is assigned to the $scriptRoot variable:
DEBUG: 12+ >>>> {
DEBUG: 14+ >>>> $scriptRoot = "HKCU:\software\ForScripting"
The ifstatement is now evaluated. If the Test-Path cmdlet is unable to find the $scriptRoot location in the registry, the ifstatement is entered, and the commands inside the associated script block will be executed.
In this example, $scriptRoot is located, and the commands inside the script block are not executed:
DEBUG: 15+ if( >>>> -not (Test-Path -path $scriptRoot))
As you see here, the Set-ItemProperty cmdlet is called on line 23 of the CreateRegistryKey.ps1 script:
DEBUG: 23+ >>>> Set-ItemProperty -Path $scriptRoot -Name $key -Value
$value | `
After the Set-ItemProperty cmdlet has executed, the script ends. The Windows PowerShell console parser now enters, with the same two lines of feedback that were shown when the tracing was first enabled:
DEBUG: 27+ >>>> } #end function Add-RegistryValue
PS C:\>
When you set the debug trace level to 1, a basic outline of the execution plan of the script is produced. This technique is good for quickly determining the outcome of branching statements (such as the ifstatement) to see if a script block is being entered. This is shown here.
DR, that is all there is to using script tracing to help debug a script. Debugging Week will continue tomorrow when I will talk about working with trace level 2.
I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson, Microsoft Scripting Guy