Windows PowerShellThe Power of Filtering
Don Jones
In last month's column, I discussed the power and flexibility of the Windows PowerShell pipeline, which lets you pass a set of data—or, more accurately, a stream of objects—from one cmdlet to another, further refining that set until it is exactly what you need it to be. In my discussion, I alluded to the fact that your own scripts,
not just cmdlets, can also take advantage of the pipeline. This month I want to discuss that topic in detail.
One of the most frequent things I do in Windows PowerShell™ is write scripts that act against multiple remote computers, usually through Windows® Management Instrumentation (WMI). As with any task that deals with remote computers, there's always the possibility that one or more of those computers won't be available when the script runs. Therefore, I need my scripts to be able to deal with that fact.
Of course, there are a number of ways in which I can give a script the ability to handle a WMI connection timeout, but I don't particularly like that approach because the timeout period itself is so long—about 30 seconds by default. This would cause my script to run much more slowly if it had to wait for numerous timeouts to occur. Instead, I want my script to perform a quick check to see if a given computer is actually online before trying to connect to it.
The Windows PowerShell Paradigm
In other scripting languages, such as VBScript, I would typically deal with one computer at a time. That is, I would retrieve a computer name—perhaps from a list of names stored in a text file—and ping the system to see if it's available. If it is available, I make the WMI connection and perform whatever other work I need to complete. This is a common scripting-style approach. In fact, I'd probably write all my code in a loop and then repeat that loop once for each computer that I need to connect to.
Windows PowerShell, however, is better suited to batch operations thanks to its basis in objects and its ability to work directly with groups or collections of objects. The paradigm in Windows PowerShell is not to work with single objects or pieces of data, but rather to work with entire groups, refining the group bit by bit until you've accomplished whatever you set out to do. For example, instead of retrieving one computer name at a time from my list, I'd read in the entire collection of names at once. And rather than pinging each computer in a loop, I'd write a single routine that accepted a collection of names, pinged them, and then output the ones that could be contacted. The next step in my process would be to make my WMI connection to the remaining names—the ones that could be reached via ping.
Windows PowerShell uses this exact approach for a number of tasks. For example, to get a list of running services, I can use something like this:
Get-Service | Where-Object { $_.Status –eq "Running" }
Figure 1 shows what the output looks like on my computer. Rather than checking each service one at a time, I retrieved all services using Get-Service, I piped those to Where-Object, and then I filtered out all those that were not running. That's more or less what I want to do with my script: get a list of computer names, filter out all the ones that are not pingable, and pass the list of pingable computers on to the next step.
Figure 1** Getting a list of pingable computers **(Click the image for a larger view)
Filtering Functions
Although I could write my own cmdlet to do this, I don't want to. Cmdlet authoring requires Visual Basic® or C# and a good amount of Microsoft® .NET Framework development expertise. More importantly, it requires more involvement than I want to invest in this task. Fortunately, Windows PowerShell lets me write a special kind of function, called a filter, that is perfectly capable of acting within the pipeline. The basic outline of a filtering function looks like this:
function <name> {
BEGIN {
#<code>
}
PROCESS {
#<code>
}
END {
#<code>
}
}
As you can see, this function contains three independent script blocks, named BEGIN, PROCESS, and END. A filtering function—that is, a function designed to work within the pipeline to filter objects—can have any combination of these three script blocks, depending on what you want to do. They work as follows:
- The BEGIN block executes once when your function is first called. You can use this to do setup work, if needed.
- The PROCESS block executes once for each pipeline object passed into the function. The $_ variable represents the current pipeline input object. The PROCESS block is required in a filtering function.
- The END block executes once after all pipeline objects have been processed. This can be used for any finalization work, if needed.
In my example, I want to produce a filtering function that will accept a collection of names as input objects, and then try to ping each one. Each one that can be pinged successfully will be output to the pipeline; systems that cannot be pinged will simply be dropped. Since the ping functionality doesn't require any special setup or finalization, I will just use the PROCESS script block. The code in Figure 2 provides the complete script.
Figure 2 Ping-Address and Restart-Computer
1 function Ping-Address {
2 PROCESS {
3 $ping = $false
4 $results = Get-WmiObject -query `
5 "SELECT * FROM Win32_PingStatus WHERE Address = '$_'"
6 foreach ($result in $results) {
7 if ($results.StatusCode -eq 0) {
8 $ping = $true
9 }
10 }
11 if ($ping -eq $true) {
12 Write-Output $_
13 }
14 }
15 }
16
17 function Restart-Computer {
18 PROCESS {
19 $computer = Get-WmiObject Win32_OperatingSystem -computer $_
20 $computer.Reboot()
21 }
22 }
23
24 Get-Content c:\computers.txt | Ping-Address | Restart-Computer
Note that I've defined two functions: Ping-Address and Restart-Computer. In Windows PowerShell, functions must be defined before they can be called. As a result, the first executable line of my script is line 24, which uses the Get-Content cmdlet to get a list of computer names from a file (one computer name per line). That list—a collection of String objects, technically—is piped to the Ping-Address function, which filters out any computers that can't be pinged. The results are piped to Restart-Computer, which uses WMI to remotely restart each computer that could be pinged.
The Ping-Address function implements a PROCESS script block, which means the function is expecting an input collection of objects. The PROCESS script block deals with that input automatically—I didn't have to define any input arguments to contain the input. I start the process on line 3 by setting the variable $ping to $false, which is a built-in Windows PowerShell variable that represents the Boolean value False.
I then use the local Win32_PingStatus WMI class to ping the specified computer. Notice on line 5 that the $_ variable, which represents the current pipeline object, is embedded inside the WMI query string. When a string is contained within double quotation marks, Windows PowerShell will always try to replace variables like $_ with their contents, so that you don't have to mess around with string concatenation. I'm using that capability on line 5.
Line 6 is a loop that checks my ping results. If any of those results come back successful (that is, with a StatusCode of zero), I set $ping equal to $true indicating success. On line 11, I check to see if $ping has been set to $true. If it has, I output the original input object to the default output stream. Windows PowerShell manages the default output stream automatically. If this function is at the end of the pipeline, then the output stream is turned into a text representation. If there are more commands in the pipeline, then the output stream's objects—in this case, string objects containing computer names—are passed into the next pipeline command.
The Restart-Computer function is somewhat simpler, but it also uses a PROCESS block so that it can easily participate in the pipeline. On line 19, the function connects to the named computer and retrieves its Win32_OperatingSystem WMI class. Line 20 executes the class's Reboot method to restart the remote computer.
Again, line 24 is where everything is actually executed. Of course, you should be very careful when running this script—it's designed to reboot every computer named in c:\computers.txt, which could definitely have disastrous results if you're not paying attention to the names in that text file!
Next Steps
This script isn't entirely bulletproof. I should also recognize the possibility of a WMI error not related to basic connectivity, such as a firewall blocking the necessary ports on the remote computer or a WMI security error. I can handle those problems by implementing a trap handler—which sounds like the perfect topic for a future column.
Also, this script is performing a fairly serious action—restarting a remote computer. Any script that attempts such a potentially hazardous action should implement two common Windows PowerShell parameters, –Confirm and –WhatIf. Explaining how to do this requires more space than I have here, but would be another great topic for a future column. In the meantime, check out the Windows PowerShell team blog; architect Jeffrey Snover covers this topic.
Even without getting into these features, you can get a good idea of what filtering functions can do. I'll refine this technique in subsequent columns, showing you how functions can provide a great way to modularize reusable code as well as enhance the functionality of your scripts in general.
Don Jones is the Lead Scripting Guru for SAPIEN Technologies and coauthor of Windows PowerShell: TFM (SAPIEN Press, 2007). Contact him at www.ScriptingAnswers.com.
© 2008 Microsoft Corporation and CMP Media, LLC. All rights reserved; reproduction in part or in whole without permission is prohibited.