Don Jones

Contents

Setting a Trap
Stop!
It's All in the Scope
Breaking Out
Why Worry?

Video (no longer available)

Continuing his discussion on building a powerful inventorying tool in Windows PowerShell, Don Jones turns his attention to error-trapping. In this month's accompanying video, Don demonstrates how you can incorporate error-handling in your Windows PowerShell-based functions.

In the previous installment of this column, I showed you how to make a rather advanced inventory tool using Windows PowerShell. The tool I created offered several options regarding output—thanks to the shell's built-in capabilities and the function's use of objects.

One admittedly weak aspect of the function I created is that it can't deal gracefully with any errors that might occur, such as connectivity or permissions issues. That is what I want to address in this month's installment of the Windows PowerShell column—I am looking at the error-handling capabilities offered by Windows PowerShell.

Setting a Trap

The Trap keyword in Windows PowerShell defines an error handler. When an exception occurs in your script, the shell looks to see if a trap has been defined—this means the trap must appear in your script before any exceptions occur. For this demonstration, I'll put together a test script that I know will generate a connectivity issue: I'll use Get-WmiObject to connect to a computer name that I know doesn't exist on the network. My goal is to have the error trap write out the bad computer name to a file, giving me a file of computer names that didn't work. I'll also include connections to two computers that are reachable (I'll use localhost). You can see the script in Figure 1.

Figure 1 Adding the trap

trap { write-host "Error connecting to $computer" -fore red "$computer" | out-file c:\demo\errors.txt -append continue } $computer = "localhost" get-wmiobject win32_operatingsystem -comp $computer $computer = "server2" get-wmiobject win32_operatingsystem -comp $computer $computer = "localhost" get-wmiobject win32_operatingsystem -comp $computer

The output from this script, shown in Figure 2, is not exactly what I am looking for. Notice that the "Error connecting to…" message didn't display. The Errors.txt file wasn't created either. In other words, my trap didn't execute at all. What happened?

fig02.gif

Figure 2 This is not the output I was hoping for!

Stop!

The key is to understand that a normal shell error message isn't the same as an exception. (There are non-terminating errors and terminating errors. Terminating errors stop the execution of the pipeline and result in an exception.) Only exceptions can be trapped. When an error occurs, the shell looks at its built-in $ErrorActionPreference variable to see what it should do. That variable defaults to having the value "Continue," which means "display an error message and keep going." Changing this variable to "Stop" would cause it to display an error message and produce a trappable exception. This, however, means any error in your script will do so.

A better technique is to have just the cmdlet you think might cause a problem use the "Stop" behavior. You can accomplish this by using the –ErrorAction (or –EA) parameter, a common parameter supported by all cmdlets. The revised version of the script is shown in Figure 3. It works just as expected, producing the output that you'll find in Figure 4.

Figure 3 Using -ErrorAction

trap { write-host "Error connecting to $computer" -fore red "$computer" | out-file c:\demo\errors.txt -append continue } $computer = "localhost" get-wmiobject win32_operatingsystem -comp $computer -ea stop $computer = "server2" get-wmiobject win32_operatingsystem -comp $computer -ea stop $computer = "localhost" get-wmiobject win32_operatingsystem -comp $computer -ea stop

Figure 4 I get more useful results when using the –ErrorAction parameter

The use of Continue at the end of the trap instructs the shell to resume execution at the line of code after the one that produced the exception. The other option is to use the keyword Break (I'll discuss that in a moment). Also notice that the $computer variable, which is defined in the script, is still valid within the trap. That's because the trap is a child scope of the script itself, meaning the trap can see all the variables within the script (more information on this in a moment, too).

It's All in the Scope

One particularly tricky aspect of error trapping in Windows PowerShell is the use of scope. The shell itself represents the global scope, which contains everything that goes on inside the shell. When you run a script, it gets its own script scope. If you define a function, the inside of the function is its own private scope, and so forth. This creates a sort of parent/child type of hierarchy.

When an exception occurs, the shell looks for a trap in the current scope. That means an exception inside a function will look for a trap inside that function. If the shell finds a trap, the shell executes the trap. If the trap ends with Continue, the shell will resume execution on the line of code that follows the one that caused the exception—remaining in the same scope. Here's a bit of pseudocode to help illustrate this:

01 Trap { 02 # Log error to a file 03 Continue 04 } 05 Get-WmiObject Win32_Service –comp "Server2" –ea "Stop" 06 Get-Process

If an exception occurs on line 5, the trap on line 1 will execute. The trap ends with Continue, so execution will resume on line 6.

Now consider this slightly different example of scope:

01 Trap { 02 # Log error to a file 03 Continue 04 } 05 06 Function MyFunction { 07 Get-WmiObject Win32_Service –comp "Server2" –ea "Stop" 08 Get-Process 09 } 10 11 MyFunction 12 Write-Host "Testing!"

If an error occurs on line 7, the shell will look for a trap within the function's scope. There isn't one, so the shell exits the function's scope and looks for a trap in the parent scope. There is a trap and so it executes at line 1. In this case, Continue will resume on the line of code in the same scope that followed the exception—that's line 12, not line 8. In other words, the shell won't re-enter the function once it's exited.

Now contrast that behavior with this example:

01 Function MyFunction { 02 Trap { 03 # Log error to a file 04 Continue 05 } 06 Get-WmiObject Win32_Service –comp "Server2" –ea "Stop" 07 Get-Process 08 } 09 10 MyFunction 11 Write-Host "Testing!"

In this case, the error on line 6 will execute the trap on line 2, staying within the function's scope. The Continue keyword will stay within that scope, resuming execution on line 7. This is the benefit to including a trap within the scope where you expect the error to occur—you remain within scope and are able to resume execution within it. But what if this method won't work in your scenario?

Cmdlet of the Month: Compare-Object

This is a cool tool for managing configuration baselines. Compare-Object, or its alias Diff, is designed to compare two sets of objects to one another. By default, it compares every property of each object, and any differences are output by the command. So imagine that you've gotten a server's services configured exactly the way you want. Just run this to create a baseline:

Get-Service | Export-CliXML c:\baseline.xml

Nearly any object can be piped to Export-CliXML, which will turn the objects into an XML file. Later, you can run the same command—such as Get-Service—and compare the results to that saved XML. Here's how:

Compare-Object (Get-Service) (Import-CliXML c:\baseline.xml) –property name

Adding the –property parameter forces the comparison to only look at that property, rather than the entire object. In this case, you'll get a list of any service names which are different from your original baseline, letting you know if any services have been added or removed since the baseline was created.

Breaking Out

Earlier, I mentioned the Break keyword. Figure 5 shows an example of how you can put the Break keyword into action.

Figure 5 Using the Break keyword

01 Trap { 02 # Handle the error 03 Continue 04 } 05 06 Function MyFunction { 07 Trap { 08 # Log error to a file 09 If ($condition) { 10 Continue 11 } Else { 12 Break 13 } 14 } 15 Get-WmiObject Win32_Service –comp "Server2" –ea "Stop" 16 Get-Process 17 } 18 19 MyFunction 20 Write-Host "Testing!"

Here's a quick overview of the chain of execution. Line 19 executes first, calling the function on line 6. Line 15 executes and generates an exception. That exception is trapped on line 7, and then on line 9 the trap must make a decision. Assuming $condition is True, the trap will continue execution on line 16.

If, however, $condition is False, the trap will Break. This exits the current scope and passes the original exception up to the parent. From the shell's view, that means line 19 produced an exception, which is trapped by line 1. The Continue keyword forces the shell to resume on line 20.

In reality, both of these traps would have a bit more code in them to deal with the error, log it, and so on. I've simply omitted that functional code in this example to make the actual flow easier to see.

Why Worry?

When do you need to implement error trapping? Only when you anticipate that an error may occur, and when you want some behavior other than a plain error message—such as logging the error to a file or displaying a more helpful error message.

I usually include error handling in more complicated scripts to help deal with errors that I can foresee happening. These include, but are not limited to, errors such as bad connectivity or permissions problems.

Error trapping definitely requires a little more effort and time to understand. As you progress into more complicated tasks within Windows PowerShell, however, error trapping will be well worth the investment to help you make a more polished and professional tool.

Don Jones is the coauthor of Windows PowerShell: TFM and author of dozens of other IT books. Reach him through his blog at concentratedtech.com.