Prof. Powershell

The PowerShell Blacksmith Part 3: Forging the Functional Tool

Time to continue sweating at the PowerShell forge, learning how to craft PowerShell-based tools. In the previous lesson we ended up with a function to take a collection of numbers and calculate the median and range. These results are written to the pipeline as a custom object. If you are jumping in the middle, take a few minutes to go back and read the first two articles (part 1 and part 2) in this mini-series. The emphasis is on practice and process not necessarily the final product.

We ended up with a working prototype. The prototype works fine is you specify the collection of numbers as a parameter, but fails when piping the collection to the function.

[Click on image for larger view.] Figure 1.

Remember, you have to think about how people will use your tool. Because I'm making a tool that is similar to Measure-Object, I need it to accept pipelined input, since that is how I think most people will use it. It is the way I pretty much always use Measure-Object.

To accomplish this, we need to modify the parameter and instruct PowerShell to recognize pipelined input.

Function Measure-Data {

[cmdletbinding()]
Param (
[Parameter(ValueFromPipeline=$True)]
$Data
)

The [cmdletbinding()] addition tells PowerShell to treat the function like a cmdlet. I know I'll want to take advantage of this later so I might as well put it in now. But the key part is the Parameter attribute which says that $Data can accept pipelined input. The syntax I've highlighted will work in PowerShell 2.0 or later. In PowerShell 3.0 and alter you can omit "=$True". The fact that it is shown implies True.

We also have to take the code and stick it inside a Process script block. Code within this script block is processed once for each object.

Function Measure-Data {

[cmdletbinding()]
Param (
[Parameter(ValueFromPipeline=$True)]
$Data
)

Process {
# code from prototype is in this script block
} #close Process

} #close measure-data

Figure 2 shows the result when I run the function. It is important to test both "ways."

[Click on image for larger view.] Figure 2.

It half-works. When used as a parameter, like the first example, $Data (in the function) contains all the elements of $a. But when piped in, the process script block runs for each element in $a so I get four results. Often that is OK and expected behavior. But in this case, my code is assuming it knows all the data points in order to calculate the median and range. So I need to add a little complexity to my function.

Cmdlets, and advanced tools like what we're building, have three script blocks: Begin, Process and End. Usually Begin and End are optional. In the Begin script block we'll put code that needs to run once before any pipelined objects are processed. Code in the End script block runs once after all the objects are processed.

In this situation, all of the code from my prototype actually needs to go into the End script block so that it can calculate on the full data set. In order for that to happen, I need to "save" each pipelined object. The easiest approach is to define an array in the Begin script block and then add each element to it in the process script block.

Function Measure-Data {

[cmdletbinding()]
Param (
[Parameter(ValueFromPipeline=$True)]
$InputObject  #<--CHANGED PARAMETER NAME
)

Begin {
    #define an array to hold incoming data
    $Data=@()
} #close Begin

Process {
    #add each incoming value to the $data array
    $Data+=$InputObject
} #close process

End {
#take incoming data and sort it
$sorted = $data | Sort-Object

#count how many elements in the array
$count = $data.Count

#region calculate median

if ($sorted.count%2) {
<#

if the number of elements is odd, add one to the count
and divide by to get middle number. But arrays start
counting at 0 so subtract one
#>

[int]$i = (($sorted.count+1)/2-1)
#get the corresponding element from the sorted array
$median = $sorted[$i]

}
else {
<#
if number of elements is even, find the average
of the two middle numbers
#>

$i = $sorted.count/2
#get the lower number
$x = $sorted[$i-1]
#get the upper number
$y = $sorted[-$i]
#average the two numbers to calculate the median
$median = ($x+$y)/2
} #else even

#endregion

#region calculate range

$range = $sorted[-1] - $sorted[0]

#endregion

#region write result

#define a hash table for the custom object
$hash = @{Median=$median;Range=$Range}

#write result object to pipeline
New-Object -TypeName PSobject -Property $hash

#endregion
} #close end

} #close measure-data

I've highlighted key changes. Because I have working code that uses $Data, I'll use that for the array, defined in the Begin script block. But in order to use it, I need to give the parameter a new name. I am using Inputobject, since that is a common parameter name and it is meaningful. In the Process script block all I have to do is add each pipelined object to the array. Testing shows much better results.

[Click on image for larger view.] Figure 3.

At this point I can continue testing but all looks well. We have a functional tool ready to use. For an apprentice tool maker you would be deservedly proud. But we're on a mission. Next time we'll add some fancy jewels and maybe a snazzy scabbard.

Click here to download a copy of the current project file.

About the Author

Jeffery Hicks is an IT veteran with over 25 years of experience, much of it spent as an IT infrastructure consultant specializing in Microsoft server technologies with an emphasis in automation and efficiency. He is a multi-year recipient of the Microsoft MVP Award in Windows PowerShell. He works today as an independent author, trainer and consultant. Jeff has written for numerous online sites and print publications, is a contributing editor at Petri.com, and a frequent speaker at technology conferences and user groups.

comments powered by Disqus
Most   Popular