Prof. Powershell
The PowerShell Blacksmith Part 2: Functions
- By Jeffery Hicks
- 10/01/2013
In the last lesson we started learning how to forge our own PowerShell tools. I started with a simple need, calculate the median and range for a set of numbers. Now that we know how to do it from the console, we need to craft a re-usable tool. In PowerShell, the best approach, at least to get started is with a function.
A PowerShell function is a reusable block of code that can be designed to behave just like a cmdlet. It is written in PowerShell's relatively easy to learn scripting language which for the most part are the commands you already know! If you want to follow along, open the PowerShell ISE.
Function Measure-Data
} #close measure-data
Here is the beginning and end of the function. We use the Function keyword and then the name of the function. You should use a standard verb and a singular noun. Don't know the verbs? At a prompt run Get-Verb and pick one that is closest to what you are trying to do. I like adding a comment with the closing curly brace. If I have a number of functions in a single file, it helps me keep track of where functions begin and end.
Next, as with any tool you have to think about how it will be used. Will someone run it as a simple command, in the same way you might run IPCONFIG? Or will the use typically piped data to it? Perhaps you need to cover both situations. You don't have to build a tool that is all things to all people. Create something that meets at least 80 percent of the typical use cases.
Next we need to plan and organize. Many people dive right it slinging code around and hoping they get things in the right order. I always recommend start by laying out in comments what it is you expect your tool to accomplish.
Function Measure-Data
#take incoming data and sort it
#count how many elements in the array
#region calculate median
<#
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
#>
#get the corresponding element from the sorted array
<#
if number of elements is even, find the average
of the two middle numbers
#>
#get the lower number
#get the upper number
#average the two numbers to calculate the median
#endregion
#region calculate range
#endregion
} #close measure-data
I know this seems like a lot of busy work, but trust me it is not. This helps you organize your process and when you are finished, the tool is properly documented. Even if someone with limited PowerShell experience were to look at your script, they should be able to follow along. That person might even be you in 6 months coming back to the script! By the way, if you are using the PowerShell 3.0 ISE the region comments will create a collapsible section. If you put related sections in regions, it makes your project a bit more manageable.
Like many beginning tool makers, there's nothing wrong with making a prototype. Something to verify your basic structure and commands work. To begin, we need a parameter for the data that we want the function to measure. Add a Param statement at the beginning of the function.
Function Measure-Data {
Param ($Data)
…
Using the commands that I know worked in the console, I can insert them into the corresponding sections of the script.
Function Measure-Data {
Param ($Data)
#take incoming data and sort it
$sorted = $data | Sort-Object
#count how many elements in the array
$count = $data.Count
#region calculate median
…
Uh-oh. I'm to the point in the function where I have to determine if there are an even or odd number of elements in $data. At the console, I figured this out by looking. But the function has to be able to accomplish this on its own. Plus there is some logic involved: "If the count is odd do this otherwise do something else".
To determine even or odd, you can use the modulo operator (%) and divide by 2.
PS C:\> 4%2
0
PS C:\> 5%2
1
As it happens, 1 and 0 can also be treated as True and False.
PS C:\> 5%2 -as [boolean]
True
This means we can use an IF statement. Here's how it looks at the console.
PS C:\> if (5%2) {"odd"} else {"even"}
odd
PS C:\> if (4%2) {"odd"} else {"even"}
Even
I like using the console for quick and dirty tests like this. In the function I can continue adding code. Here's what I have so far.
Function Measure-Data {
Param ($Data)
#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
} #close measure-data
Time to take our first swing with it. In the PowerShell ISE you can run the script file which will load the function into the shell. But when I try to run it with some test data, it fails. Sort of.
PS C:\> measure-data $a
PS C:\>
In the function I defined variables for the results but never wrote them back to the pipeline. I'm a strong believer that functions should write a single type of object back to the pipeline. At the end of my function I'll some code to create a custom object using a hash table. Here's the final function with my additions highlighted.
Function Measure-Data {
Param ($Data)
#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 measure-data
I'm testing with numbers where I already know what the results should be and this looks good. Remember, this is only a prototype. In out next lesson we'll begin crafting a more polished product. In the meantime, if you want a copy of the current prototype, click here.
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.