Thursday, October 9, 2008

Emitting Objects in PowerShell (aka, the Evolution of Functions) | Concentrated Technology

 

Lunch Lesson: Emitting Objects in PowerShell (aka, the Evolution of Functions)

 

Probably one of the most important things you can learn to do in Windows PowerShell is write scripts and functions that emit not text, but rather emit custom objects. For example, let’s take a very simple script that uses WMI to ping a remote computer:

$computer = ’server2′

$results = Get-WmiObject -query “SELECT * FROM Win32_PingStatus WHERE Address = ‘$computer’”

if ($results.StatusCode -eq 0) {

  Write-Host “$computer is Pingable”

} else {

  Write-Host “$computer is not Pingable”

}

This simply writes a text message directly to the console window. Now, there are a couple of problems with this approach. One is that the output isn’t terribly re-usable. For example, what if you wanted to use this bit of code to test a computer’s connectivity before attempting to connect to it for some other purpose, such as remote management? In that case, you might prefer a True or False output, since that could be used to let a larger script determine whether or not to try connecting. Another problem with this is that it doesn’t accept pipeline input - the computer name you’re pinging is in a variable, meaning it’s hardcoded - it’d be nice to not only have this parameterized, but also designed to accept many computer names from the pipeline. So let’s make a few revisions.

function Ping-Host {

  param([string]$computer = ‘localhost’)

  $results = get-wmiobject -query “SELECT * FROM Win32_PingStatus WHERE Address = ‘$computer’”

  if ($results.StatusCode -eq 0) {

    Write-Output $True

  } else {

    Write-Output $False

  }

}

This parameterized function can be used as follows:

if (Ping-Host “server2″) {

  # do something - it’s pingable

}

Or, if we wanted to read a bunch of computer names from a file (which includes one computer name per line), we could do this:

$names = Get-Content c:\computers.txt

foreach ($name in $names) {

  if (Ping-Host $name) {

    # do something - it’s pingable

  }

}

In fact the above is a very VBScript-style approach to the problem: Get a bunch of items (computer names) and then enumerate through them one at a time. But PowerShell is designed to circumvent a lot of that complexity by offering a pipeline that will work with batches of objects, rather than forcing you to manually enumerate through them like this. Redoing this function to support pipeline input isn’t difficult:

function Ping-Host {

  PROCESS {

    $results = get-wmiobject -query “SELECT * FROM Win32_PingStatus WHERE Address = ‘$_’”

    if ($results.StatusCode -eq 0) {

      Write-Output $_

    }

  }

}

Really, there are only four changes here:

  • We’ve wrapped the function’s code in a scriptblock named PROCESS
  • We removed the PARAM declaration
  • Instead of using the $computer parameter to carry the computer name, we’re using $_ - this variable will be automatically populated by Windows PowerShell with whatever comes in from the pipeline.
  • Rather than writing out True and False values, we’re just writing out the computer names of the computers which are pingable. Compuetrs which aren’t pingable disappear - making our Ping-Host function act as a “filtering function,” filtering out non-reachable computers.

Now we can use this in a pipeline that accepts a bunch of computer names, filters out those which aren’t reachable, and leaves us with the ones that are:

Get-Content c:\computers.txt | Ping-Host

We could then pipe those reachable computer names on to some other cmdlet to actually do something with them. But wait… there are still some problems, here. For one, we’re losing information: If a computer isn’t reachable, the function just drops it like a bad habit. What if we wanted to do something with only the non-reachable computers, like send a Wake-On-LAN packet? We’d have to change the way our function works, meaning the function isn’t as reusable as it could be. Let’s make a revision.

function Ping-Host {

  BEGIN {

    Write-Output “Computer`tResponseTime`tReachable`tIPAddress”

  }

  PROCESS {

    $results = get-wmiobject -query “SELECT * FROM Win32_PingStatus WHERE Address = ‘$_’”

    $template = “{0}`t{1}`t{2}`t{3}”

    if ($results.StatusCode -eq 0) {

      Write-Output ($template -f $_,($results.ResponseTime),$True,($results.ProtocolAddress))

    } else {

      Write-Output ($template -f $_,($results.ResponseTime),$False,($results.ProtocolAddress))

    }

  }

}

This uses some pretty powerful juju, so let’s analyze it:

  • The BEGIN scriptblock runs first, and emits a header. The `t bits insert tabs to create columns.
  • $template is just a template output string, with {tokens} separated by tabs
  • the -f operator is used to insert data into the {tokens}. We’re inserting the original computer name ($_), the response time from the ping, either $True or $False if it’s reachable or not, and finally the ProtocolAddress property from the ping - that’s the IP address which responded to the ping.

So in the end we wind up with a formatted table of output. Well, sort of - using tabs to format output doesn’t usually work out great. We’ll spend a lot of time fussing with it, but the fact is we’ve done a bad thing. We’ve got PowerShell outputting text, and we’re doing all of the formatting work ourselves. Why would we do that when PowerShell has a much better formatting subsystem than we could ever write? Plus, how would we re-use this output? If we run:

Get-Content c:\computers.txt | Ping-Host

What we get is a bunch of text - in order to filter out the computers which were (or were not) reachable, we’ll have to parse that text, which is a major pain. We’d have to parse it again to extract the computer names from that output, in order to use those computer names for some subsequent process. Outputting text in Windows PowerShell leads to the Dark Side: More work for you. Who needs more work?

The solution is to have our function output objects, not text.

function Ping-Host {

  PROCESS {

    $results = get-wmiobject -query “SELECT * FROM Win32_PingStatus WHERE Address = ‘$_’”

    $obj = New-Object PSObject

    $obj | Add-Member NoteProperty Name $_

    $obj | Add-Member NoteProperty ResponseTime ($results.ResponseTime)

    $obj | Add-Member NoteProperty Address ($results.ProtocolAddress)

    if ($results.StatusCode -eq 0) {

      $obj | Add-Member NoteProperty Responding $True

    } else {

      $obj | Add-Member NoteProperty Responding $False

    }

    Write-Output $obj

  }

}

Welcome to the Light Side.

  • We start by creating a new, blank object of the PSObject type. That’s basically a blank canvas.
  • We then attach four properties to the object: The computer name, the response time, and the IP address. We then attach a “responding” property with either True or False as its value.
  • We output the object to the pipeline by using Write-Output.

Now we can get a nicely-formatted table:

Get-Content c:\computers.txt | Ping-Host | Format-Table

If we just want the computers which aren’t reachable:

Get-Content c:\computers.txt | Ping-Host | Where { $_.Responding -eq $False }

If we want only reachable computers, sorted by response time:

Get-Content c:\computers.txt | Ping-Host | Where { $_.Responding -eq $True } | Sort ResponseTime -descending

If we want to output just the computer names and response times to a CSV file:

Get-Content c:\computers.txt | Ping-Host | Select Name,ResponseTime | Export-CSV Pings.csv

The point is that by outputting objects which contain all the information we might ever need, we can reuse this one function in a variety of situations without ever having to make changes to it. We can utilize PowerShell’s rich functionality to filter and manipulate our output, convert and export it to other formats, and so forth. So your goal should be to produce rich objects whenever possible, so that you’ll get the biggest long-term return on your scripting investment.

Lunch Lesson: Emitting Objects in PowerShell (aka, the Evolution of Functions) | Concentrated Technology

No comments:

Blog Archive