Quick and Efficient PowerShell Script Debugging with Breakpoints
You've created a complex PowerShell script, executed it, and it works great in your test environment. Yay! Then a couple days go by and you try your great new script in production—and it bombs miserably in five different places. Not so yay. What went wrong? What's different in your test environment versus your production environment? Why is this beautiful script of yours failing?
Questions, questions, questions. It's time to get down to debugging.
As a start to tracking down problems in your script, PowerShell provides several debugging features. One feature is breakpoints, which have been around for a very long time, and are not new to PowerShell. In simplest terms, a breakpoint is a spot in your script that, when executed, will break (pause) the script's execution. When a PowerShell script hits a breakpoint, it will pause execution and wait for input.
Depending on how the breakpoint was created, you get the option to poke around in your script's current running environment. You can discover the progress of the script up to the breakpoint, review the history of functions called, and a lot more. Breakpoints are an excellent way to peek inside a script at a certain point in time to see what's happening.
Breakpoints can be created in PowerShell in one of two ways: by using the Write-Debug cmdlet, or by using the Set-PsBreakPoint cmdlet. Let's examine how these methods work.
Using Write-Debug to Insert Breakpoints
Of the two methods, Write-Debug is the little brother to Set-PsBreakpoint. It is limited in scope and use, but it pauses a PowerShell script and provides some methods to see what might have gone wrong with the script.
Write-Debug requires a few flags to be set:
- The script or function must be "advanced," which means you must include the [CmdletBinding()] keyword. If this keyword is not included, any Write-Debug lines you may use will simply be ignored.
- The $DebugPreference automatic variable must be set to Inquire, or the -Debug parameter must be passed to the script or function.
Let's look at an example. I've built a script, shown in Figure 1, that creates a few fictional users. Before each user is created, the script has a Write-Debug line that sets a breakpoint, so I can confirm that I actually want to create the users. Notice the required [CmdletBinding()] keyword at the top of the figure.
Figure 1 Using [CmdletBinding()] and Write-Debug.
If I run this script, I get no output at all. It simply runs through and does nothing, as Figure 2 shows. Why?
Figure 2 By default, no output.
The problem is either that I didn't have $DebugPreference set correctly, or I didn't use the -Debug parameter.
Let's first address $DebugPreference. By default, $DebugPreference is set to SilentlyContinue. The example in Figure 2 shows you exactly what that setting does. When faced with a Write-Debug line, it silently continues, doing nothing. Let's change $DebugPreference to Inquire, which will force any Write-Debug line to set a breakpoint and give the user some options for continuing, as shown in Figure 3.
Figure 3 $DebugPreference has triggered the breakpoint.
Even though I expected NewUser.ps1 to behave exactly as it did the last time I used the script, Write-Debug actually triggered a breakpoint, prompting me for a choice of what to do. I can confirm that I'd like to continue the operation for this individual ([Y]), or go ahead and confirm them all ([A]), or halt script execution and don't continue ([H]), or suspend the pipeline ([S])and give control back to the host. I chose to suspend the operation, as shown in Figure 4. The arrow in the figure indicates how you can tell the pipeline is suspended.
Figure 4 Double arrows (>>) indicate a suspended pipeline.
Now let's say $DebugPreference is still set to SilentlyContinue, as shown in Figure 5. The first few lines indicate that simply executing NewUser.ps1 with SilentlyContinue brings about no output. However, if I add a -Debug parameter, the script is triggered by that breakpoint now.
Figure 5 Using the -Debug parameter.
The -Debug parameter is created automatically as soon as you add the [CmdletBinding()] keyword. It's used as a way of essentially overriding $DebugPreference and forcing the script to halt at any breakpoint that it sees.
Using Set-PsBreakPoint to Insert Breakpoints
Once you've mastered setting breakpoints with Write-Debug, it's time to move up to the big leagues with Set-PsBreakPoint. This cmdlet not only sets breakpoints, but also gives you more options for controlling when the breakpoint is triggered, so you can peruse the script's environment at the time the breakpoint was set.
Unlike Write-Debug, Set-PsBreakPoint has three options for setting the breakpoint: on a particular line, when a variable is used, or when a certain command is executed inside the script (see Figure 6).
Figure 6 Set-PSBreakpoint options.
Set-PsBreakPoint also differs from Write-Debug in how the cmdlet is used. Set-PsBreakPoint is typically used outside the actual script, rather than inside the script itself.
Let's use our previous script example to compare the cmdlets. In Figure 7, I've set a breakpoint on line 7 using Write-Debug.
Figure 7 Using Write-Debug to set a breakpoint.
I can set the same breakpoint using Set-PsBreakPoint, without having to muck up my script with a bunch of Write-Debug lines. I simply set the breakpoint in my current PowerShell session (see Figure 8). PowerShell monitors my NewUser.ps1 script and triggers the breakpoint as soon as the script hits line 7.
Figure 8 Triggering a breakpoint on a specific line number.
The console output changes when a script hits a breakpoint created with Set-PsBreakPoint. You come to a [DBG] prompt, as shown in Figure 9. This separates Set-PsBreakPoint from Write-Debug; not only does Set-PsBreakPoint set a breakpoint, but it provides you with a debugging session where you have control of how to proceed, as shown in Figure 10.
Figure 9 The debugger menu.
Figure 10 Debugger menu options.
The debugging menu gives you the option to stepInto the next step in the script in a controlled manner (see Figure 11).
Figure 11 Stepping into code via the debugger.
You also have the option to see where you are in the script with the list (l) option. The arrow in Figure 12 points out the asterisk (*) that indicates where in the script the breakpoint occurred.
Figure 12 The breakpoint has occurred at line 7.
This menu offers a lot of options, giving you much more control over how the script's execution behaves when you're debugging. This feature makes Set-PsBreakPoint the big brother of Write-Debug.
Final Thoughts
We all hope never to need to debug a PowerShell script, but fallible humans make mistakes. Unless it's extremely simple, you probably will never write a perfect script the first time. Debugging is an important skill, and breakpoints are a critical component of debugging.
While you're in the mood to learn about breakpoints, check out the other cmdlets related to breakpoints, such as Enable-PsBreakpoint and Disable-PsBreakpoint. If you can master breakpoints in PowerShell, you'll be well on your way to becoming a master PowerShell debugger. If you want further information about the debugger, read Microsoft TechNet's great article "The Windows PowerShell Debugger." It has a ton of great information and dives deep into many of the features we don't have time to go over here.