Debugging in Windows PowerShell
How to squash existing bugs and prevent new infestations
September 20, 2010
If you ever tried your hand at writing a PowerShell script, you likely had to spend some time debugging it. Bugs are an inevitable part of life when you're trying to tell a computer something to do. On the surface, PowerShell doesn't seem to offer much in the way of debugging assistance. In this short guide, I'll tell you about some of the basic techniques for debugging and some practices that can help keep the bugs at bay.
Although PowerShell doesn't have any kind of one-line-at-a-time graphical debugger built in, there are free and commercial third-party tools that do. Editors such as Quest Software's PowerGUI, Idera's PowerShellPlus, and SAPIEN Technologies' PrimalScript all use different techniques to trick PowerShell into running your script one line at a time, showing you what a script's variables contain and generally making debugging easier. However, unless you know what debugging is all about, these tools don't make you a more effective debugger.
Before you start debugging, you need to know what you're looking for—in other words, what is a bug? There are two distinct types.
Bug Type One: Fat Fingers
The first type of bug is a simple syntax error. A typo. An operator malfunction. Fat fingers. Pick a term. You'll find these bugs relatively easy to spot because PowerShell will hurl an error in your direction, complete with the number of the line that contains the error. However, if you're using Notepad, knowing the line number won't help because it doesn't display line numbers. For this reason, you should stop using Notepad and use PowerGUI, which is free, or a commercial script editor instead.
Commercial script editors offer more than just line numbering, and some of their features can help prevent fat-finger bugs. Chief among these capabilities is syntax highlighting, which is nothing fancier than making your script code turn colors. But the key is that things only turn the right color when they're spelled correctly. So, if you're using a syntax-highlighting editor, start becoming familiar with the colors it uses for command names, variables, literal strings, and so forth. If they aren't turning the right color as you type, you missed something. Go back and look carefully for the problem.
If you do miss something, PowerShell will be more than willing to tell you about it. Pay attention to error messages. I can't tell you how often I see admins struggling to fix something, simply because they're not reading the error message. When they see that red text hit the screen, they go into a bit of a panic and just start trying different things. But the error message is telling you what line of code has the problem, and most of the time it's even telling you what the problem is. So just slow down a bit, read carefully, comprehend what the error is saying, and see if you can spot the problem.
A good script editor can provide further help by asking PowerShell to run a sort of preflight checklist on your script. This feature is typically called live syntax checking. The editor can then call your attention to simple errors right away, before you even run the script. Some use a red underline (like Microsoft Word's spell-check feature); others use different visual indicators.
Finally, adopt some best practices to keep yourself out of trouble. Format your code nicely so that constructs are indented and their curly braces line up, as in
Function Get-Something{Code here}
rather than messy code like this
Function Get-Something { # Code here }
The first example is easier to read and easier to verify that there's an opening and closing brace. When you start nesting constructs, it's easy to miss a closing brace when code is set up like the second example.
Bug Type Two: Expectations Don't Match Reality
The second type of bug has to do with expectations not being met. With these bugs, your script runs without complaint, but it doesn't do what you think it should. PowerShell typically doesn't return an error message with these types of bugs, or it returns an error message that doesn't make sense. There are only two reasons why this type of bug exists:
You made a logic error. People sometimes make basic logic errors. For example, let's say you want your script to count from 1 to 10 so you run the code
For ($i=0; $i -lt 10; $i++){ Write-Host $i}
All goes well when the script runs, except that the results show 0 to 9. Close, but not quite what you wanted. This is a case where slowing down and thinking like the shell can pay off. "Thinking like the shell" means pretending you're PowerShell while reading the script, taking the time to write down what the variables should contain at each step as well as each step's results. For the script just given, the run-down might go as follows:
I first need to initialize $i to 0 (write down $i=0). Is 0 less than 10? Yes, so I'll use Write-Host to display the value of 0 (write down result is 0) and increment $i by one. At this point, $i contains 1 (write down $i=1). Is 1 less than 10? Yes, so I'll display the value of 1 (write down result is 1) and increment $i by one. Now, $i contains 2 (write down $i=2), which is less than 10, so I'll display that number (write down result is 2), and so on.
This would continue until $i contains 10. At this point, you'll find that 10 is not less than 10, which is why PowerShell didn't display that number. As this example shows, most basic logic errors become obvious when you take the time to do this simple run-through.
You made an assumption. Thinking like the shell can solve not only basic logic errors but also more serious logic errors because the paper record provides a list of your expectations. Let's say you've run through your script in your head and written down what you think should happen, but when you run your script, something else happens. I refer to this kind of bug as a "bad expectation" or "lack of knowledge" bug. The cause is simple: A variable, property value, or method result doesn't contain what you thought it would contain. Because you've already done the act of "thinking like the shell," you have a written record of your expectations. So, all you need to do is have PowerShell generate the same kind of list, then see where its results differ from your expectations. When you find the difference, you'll have found the bug.
There are two basic techniques for getting PowerShell to generate the same kind of list you created by thinking like the shell. The techniques involve using the Write-Debug and SetPSDebug cmdlets. There's actually a third technique, which involves using a script editor's interactive debugger. That experience is basically just a roll-up of the two underlying techniques I'm about to cover—and I want you to know the underlying techniques before you start taking the easier road with an editor's interactive debugger.
Technique 1: Use Write-Debug
The first technique involves using the WriteDebug cmdlet. By default, output from this cmdlet is suppressed. To enable that output, you need to add the line
$DebugPreference = "Continue"
to the top of your script.
Then, you need to start adding WriteDebug statements immediately after any statements that change a variable's contents, as in
$var = Get-ServiceWrite-Debug "`$var contains $var"
Notice the little trick I used: I enclosed the WriteDebug cmdlet's output in double quotes, which tells PowerShell to replace the variables with their actual contents. For the first variable, though, I used the backtick (PowerShell's escape character) so that the $ was escaped. This will result in the first $var being displayed as-is, so you can see the variable name. The second $var will be replaced with its contents.
Next, you need to add a WriteDebug statement in every loop and decision construct. Listing 1 shows examples of how to add it to a for loop and if construct.
for ($i=0; $i -lt 10; $i++){ # BEGIN CALLOUT A Write-Debug "`$i is $i" # END CALLOUT A if ($i –gt 2) { # Do something here. # BEGIN CALLOUT B Write-Debug '$i is greater than 2' } else { Write-Debug '$i is not greater than 2' } # END CALLOUT B}
In the Write-Debug statement in callout A, I use the same double-quotes-and-backtick trick to find out what $i contains in each iteration of the for loop. In the if construct, I use two Write-Debug statements in an else construct, as callout B shows. With this setup, I get debug output no matter which way the decision goes, even though I don't have any code to execute if $i is not greater than 2. Notice that I use single quotes for the Write-Debug statements' output so that the $i variable wouldn't be replaced with its contents.
After all the Write-Debug statements have been added, it's time to run the script and compare the debug output to your written expectations. Look for any differences. Perhaps you'll need to revise your expectations, but any differences are a likely place for a bug to live.
When your script is running as expected, simply change $DebugPreference at the top of your script to
$DebugPreference = "SilentlyContinue"
and your debug output will be suppressed again. There's no need to remove the WriteDebug statements. Leaving them in will make debugging the script easier in the future.
Technique 2: Use Set-PSDebug
The second technique involves using PowerShell's built-in step debugger, Set-PSDebug, to go through your script one line at a time. This can be a bit tedious in long scripts, but in PowerShell 1.0, it's what you have to work with. (PowerShell 2.0 also supports breakpoints, which I'll be covering in a future article. They allow you to define when your script will enter suspend mode, rather than forcing you to step through it one line at a time.)
To enable the debugger, run the command
SetPSDebug –step
You can disable it later by running
SetPSDebug –off
Once enabled, the debugger is effective for all scripts. (It also works on commands entered on the command line.) You just run your script and start debugging. Each time PowerShell encounters a line in your script, you'll see that line displayed and PowerShell will ask if you want to continue. (I like having a line-numbered printout of my script handy so that I can see exactly where PowerShell is.) To run a line, press Enter. This is where you can start comparing your expectations to what PowerShell actually does.
Any time your script changes a variable or is about to use a variable or object property, don't press Enter. Instead, type S then press Enter. This suspends your script and presents you with a special prompt. In it, you can:
Access variables to see what they contain
Run commands to see what they produce
View object properties to see what they contain
When you find a difference between the shell's results and your expectations, you've found a bug. Run the Exit command to leave the suspend mode and resume script execution right where you left off.
The step debugger takes a bit of time to get used to. However, it can be useful to get inside your script while it's actually running.
Ready to Debug?
Debugging can seem painful, but it doesn't need to be, provided you're methodical, consistent, and patient in your approach. When you receive an error message, don't let the red text panic you. Take the time to read and understand it. Treat it as a friend that just wants to help (albeit in a somewhat garish, annoying manner).
When you don't get the results you expect, take the time to document your expectations and find where your expectations don't match reality. The number one cause of bugs is someone who has no idea what to expect from the script at each step of the way. That's common for a newcomers who are trying to, for example, use a script they found on the Internet. By taking the time to understand a script, you'll not only be helping yourself debug faster but also helping yourself learn more about PowerShell and become more effective at writing your own scripts in the future. In that regard, debugging can be an investment that's well worth the payoff.
About the Author
You May Also Like