Chapter 9. Functions

by Mar 17, 2012

Functions work pretty much like macros. As such, you can attach a script block to a name to create your own new commands.

Functions provide the interface between your code and the user. They can define parameters, parameter types, and even provide help, much like cmdlets.

In this chapter, you will learn how to create your own functions.

Topics Covered:

Creating New Functions

The most simplistic function consists of a name and a script block. Whenever you call that name, the script block executes. Let's create a function that reads installed software from your registry.

First, define the function body. It could look like this:

function Get-InstalledSoftware {

}

Once you enter this code in your script editor and run it dot-sourced, PowerShell learned a new command called Get-InstalledSoftware. If you saved your code in a file called c:somescript.ps1, you will need to run it like this:

. 'c:somescript.ps1'

If you don't want to use a script, you can also enter a function definition directly into your interactive PowerShell console like this:

function Get-InstalledSoftware {  }

However, defining functions in a script is a better approach because you won't want to enter your functions manually all the time. Running a script to define the functions is much more practical. You may want to enable script execution if you are unable to run a script because of your current ExecutionPolicy settings:

Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -force

Once you defined your function, you can even use code completion. If you enter "Get-Ins" and then press TAB, PowerShell will complete your function name. Of course, the new command Get-InstalledSoftware won't do anything yet. The script block you attached to your function name was empty. You can add whatever code you want to run to make your function do something useful. Here is the beef to your function that makes it report installed software:

function Get-InstalledSoftware {
  $path = 'Registry::HKEY_LOCAL_MACHINESoftwareMicrosoftWindowsCurrentVersionUninstall*'
  Get-ItemProperty -path $path |
    Where-Object { $_.DisplayName -ne $null } |
    Select-Object DisplayName, DisplayVersion, UninstallString |
    Sort-Object DisplayName
}

When you run it, it will return a sorted list of all the installed software packages, their version, and their uninstall information:

PS > Get-InstalledSoftware

DisplayName                         DisplayVersion                UninstallString
-----------                         --------------                ---------------
64 Bit HP CIO Components Installer  8.2.1                         MsiExec.exe /I{5737101A-27C4-40...
Apple Mobile Device Support         3.3.0.69                      MsiExec.exe /I{963BFE7E-C350-43...
Bonjour                             2.0.4.0                       MsiExec.exe /X{E4F5E48E-7155-4C...
(...)

As always, information may be clipped. You can pipe the results to any of the formatting cmdlets to change because the information returned by your function will behave just like information returned from any cmdlet.

Note the way functions return their results: anything you leave behind will be automatically assigned as return value. If you leave behind more than one piece of information, it will be returned as an array:

PS > function test { "One" }
PS > test
One
PS > function test { "Zero", "One", "Two", "Three" }
PS > test
Zero
One
Two
Three
PS > $result = test
PS > $result[0]
Zero
PS > $result[1,2]
One
Two
PS > $result[-1]
Three

Defining Function Parameters

Some functions, such as Get-InstalledSoftware in the previous example, will work without additional information from the user. From working with cmdlets, you already know how clever it can be to provide detailed information so the command can return exactly what you want. So, let's try adding some parameters to our function.

Adding parameters is very simple. You can either add them in parenthesis right behind the function name or move the list of parameters inside your function and label this part param. Both definitions define the same function:

function Speak-Text ($text) {
  (New-Object -com SAPI.SPVoice).Speak($text) | Out-Null
}

function Speak-Text {
 param ($text)

  (New-Object -com SAPI.SPVoice).Speak($text) | Out-Null
}

Your new command Speak-Text converts (English) text to spoken language. It accesses an internal Text-to-Speech-API, so you can now try this:

Speak-Text 'Hello, I am hungry!'

Since the function Speak-Text now supports a parameter, it is easy to submit additional information to the function code. PowerShell will take care of parameter parsing, and the same rules apply that you already know from cmdlets. You can submit arguments as named parameters, as abbreviated named parameters, and as positional parameters:

Speak-Text 'This is positional'
Speak-Text -text 'This is named'
Speak-Text -t 'This is abbreviated named'

To submit more than one parameter, you can add more parameters as comma-separated list. Let's add some parameters to Get-InstalledSoftware to make it more useful. Here, we add parameters to select the product and when it was installed:

function Get-InstalledSoftware {
  param(
    $name = '*',
    
    $days = 2000
  )
  
  $cutoff = (Get-Date) - (New-TimeSpan -days $days)
  $cutoffstring = Get-Date -date $cutoff -format 'yyyyMMdd'
  
  $path = 'Registry::HKEY_LOCAL_MACHINESoftwareMicrosoftWindowsCurrentVersionUninstall*'
  $column_days = @{
    Name='Days'
    Expression={ 
          if  ($_.InstallDate) { 
            (New-TimeSpan ([DateTime]::ParseExact($_.InstallDate, 'yyyyMMdd', $null))).Days
            } else { 'n/a' }
        }
    }
  Get-ItemProperty -path $path |
    Where-Object { $_.DisplayName -ne $null } |
    Where-Object { $_.DisplayName -like $name } |
    Where-Object { $_.InstallDate -gt $cutoffstring } |
    Select-Object DisplayName, $column_Days, DisplayVersion |
    Sort-Object DisplayName
}

Now, Get-InstalledSoftware supports two optional parameters called -Name and -Days. You do not have to submit them since they are optional. If you don't, they are set to their default values. So when you run Get-InstalledSoftware, you will get all software installed within the past 2,000 days. If you want to only find software with "Microsoft" in its name that was installed within the past 180 days, you can submit parameters:

PS > Get-InstalledSoftware -name *Microsoft* -days 180 | Format-Table -AutoSize

DisplayName                                       Days DisplayVersion
-----------                                       ---- --------------
Microsoft .NET Framework 4 Client Profile           38 4.0.30319
Microsoft Antimalware                              119 3.0.8107.0
Microsoft Antimalware Service DE-DE Language Pack  119 3.0.8107.0
Microsoft Security Client                          119 2.0.0657.0
Microsoft Security Client DE-DE Language Pack      119 2.0.0657.0
Microsoft Security Essentials                      119 2.0.657.0
Microsoft SQL Server Compact 3.5 SP2 x64 ENU        33 3.5.8080.0

Adding Mandatory Parameters

Let's assume you want to create a function that converts dollars to Euros . Here is a simple version:

function ConvertTo-Euro {
  param(
    $dollar,

    $rate=1.37
  )

  $dollar * $rate
}

And here is how you run your new command:

PS > ConvertTo-Euro -dollar 200
274

Since -rate is an optional parameter with a default value, there is no need for you to submit it unless you want to override the default value:

PS > ConvertTo-Euro -dollar 200 -rate 1.21
242

So, what happens when the user does not submit any parameter since -dollar is optional as well? Well, since you did not submit anything, you get back nothing.

This function can only make sense if there was some information passed to $dollar, which is why this parameter needs to be mandatory. Here is how you declare it mandatory:

function ConvertTo-Euro {
  param(
    [Parameter(Mandatory=$true)]
    $dollar,

    $rate=1.37
  )

  $dollar * $rate
}

This works because PowerShell will ask for it when you do not submit the -dollar parameter:

PS > ConvertTo-Euro -rate 6.7

cmdlet ConvertTo-Euro at command pipeline position 1
Supply values for the following parameters:
dollar: 100
100100100100100100100

However, the result looks strange because when you enter information via a prompt, PowerShell will treat it as string (text) information, and when you multiply texts, they are repeated. So whenever you declare a parameter as mandatory, you are taking the chance that the user will omit it and gets prompted for it. So, you always need to make sure that you declare the target type you are expecting:

function ConvertTo-Euro {
  param(
    [Parameter(Mandatory=$true)]
    [Double]
    $dollar,

    $rate=1.37
  )

  $dollar * $rate
}

Now, the function performs as expected:

PS > ConvertTo-Euro -rate 6.7

cmdlet ConvertTo-Euro at command pipeline position 1
Supply values for the following parameters:
dollar: 100
670

Adding Switch Parameters

There is one parameter type that is special: switch parameters do not take arguments. They are either present or not. You can assign the type [Switch] to that parameter to add switch parameters to your function. If you wanted to provide a way for your users to distinguish between raw and pretty output, your currency converter could implement a switch parameter called -Pretty. When present, the output would come as a nice text line, and when it is not present, it would be the raw numeric value:

function ConvertTo-Euro {
  param(
    [Parameter(Mandatory=$true)]
    [Double]
    $dollar,

    $rate=1.37,
    
    [switch]
    $pretty
  )

  $result = $dollar * $rate
  
  if ($pretty) {
    '${0:0.00} equals EUR{1:0.00} at a rate of {2:0:0.00}' -f 
      $dollar, $result, $rate
  } else {
    $result
  }
}

Now, it is up to your user to decide which output to choose:

PS > ConvertTo-Euro -dollar 200 -rate 1.28
256
PS > ConvertTo-Euro -dollar 200 -rate 1.28 -pretty
$200,00 equals EUR256,00 at a rate of 1.28

Adding Help to your Functions

Get-Help returns Help information for all of your cmdlets. It can also return Help information for your self-defined functions. All you will need to do is add the Help text. To do that, add a specially formatted comment block right before the function or at the beginning or end of the function script block:

<#
.SYNOPSIS
   Converts Dollar to Euro
.DESCRIPTION
   Takes dollars and calculates the value in Euro by applying an exchange rate
.PARAMETER dollar
   the dollar amount. This parameter is mandatory.
.PARAMETER rate
   the exchange rate. The default value is set to 1.37.
.EXAMPLE
   ConvertTo-Euro 100
   converts 100 dollars using the default exchange rate and positional parameters
.EXAMPLE
   ConvertTo-Euro 100 -rate 2.3
   converts 100 dollars using a custom exchange rate
#>

function ConvertTo-Euro {
  param(
    [Parameter(Mandatory=$true)]
    [Double]
    $dollar,

    $rate=1.37,
    
    [switch]
    $pretty
  )

  $result = $dollar * $rate
  
  if ($pretty) {
    '${0:0.00} equals EUR{1:0.00} at a rate of {2:0:0.00}' -f 
      $dollar, $result, $rate
  } else {
    $result
  }
}

Note that the comment-based Help block may not be separated by more than one blank line if you place it above the function. If you did everything right, you will now be able to get the same rich help like with cmdlets after running the code:

PS > ConvertTo-Euro -?

NAME
    ConvertTo-Euro

SYNOPSIS
    Converts Dollar to Euro


SYNTAX
    ConvertTo-Euro [-dollar] <Double> [[-rate] <Object>] [-pretty] [<CommonParameters>]


DESCRIPTION
    Takes dollars and calculates the value in Euro by applying an exchange rate


RELATED LINKS

REMARKS
    To see the examples, type: "get-help ConvertTo-Euro -examples".
    for more information, type: "get-help ConvertTo-Euro -detailed".
    for technical information, type: "get-help ConvertTo-Euro -full".



PS > Get-Help -name ConvertTo-Euro -Examples

NAME
    ConvertTo-Euro

SYNOPSIS
    Converts Dollar to Euro

    -------------------------- EXAMPLE 1 --------------------------

    C:PS>ConvertTo-Euro 100


    converts 100 dollars using the default exchange rate and positional parameters




    -------------------------- EXAMPLE 2 --------------------------

    C:PS>ConvertTo-Euro 100 -rate 2.3


    converts 100 dollars using a custom exchange rate







PS > Get-Help -name ConvertTo-Euro -Parameter *

-dollar <Double>
    the dollar amount. This parameter is mandatory.

    Required?                    true
    Position?                    1
    Default value
    Accept pipeline input?       false
    Accept wildcard characters?


-rate <Object>
    the exchange rate. The default value is set to 1.37.

    Required?                    false
    Position?                    2
    Default value
    Accept pipeline input?       false
    Accept wildcard characters?


-pretty [<SwitchParameter>]

    Required?                    false
    Position?                    named
    Default value
    Accept pipeline input?       false
    Accept wildcard characters?

Creating Pipeline-Aware Functions

Your function is not yet pipeline aware/ So, it will simply ignore the results delivered by the upstream cmdlet if you call it within a pipeline statement:

1..10 | ConvertTo-Euro 

Instead, you will receive exceptions complaining about PowerShell not being able to "bind" the input object. That's because PowerShell cannot know which parameter is supposed to receive the incoming pipeline values. If you want your function to be pipeline aware, you can fix it by choosing the parameter that is to receive the pipeline input. Here is the enhanced param block:

function ConvertTo-Euro {
  param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [Double]
    $dollar,

    $rate=1.37,
    
    [switch]
    $pretty
  )
...

By adding ValueFromPipeline=$true, you are telling PowerShell that the parameter -dollar is to receive incoming pipeline input. When you rerun the script and then try the pipeline again, there are no more exceptions. Your function will only process the last incoming result, though:

PS > 1..10 | ConvertTo-Euro
13,7

This is because functions will by default execute all code at the end of a pipeline. If you want the code to process each incoming pipeline data, you must assign the code manually to a process script block or rename your function into a filter (by exchanging the keyword function by filter). Filters will by default execute all code in a process block.

Here is how you move the code into a process block to make a function process all incoming pipeline values:

<#
.SYNOPSIS
   Converts Dollar to Euro
.DESCRIPTION
   Takes dollars and calculates the value in Euro by applying an exchange rate
.PARAMETER dollar
   the dollar amount. This parameter is mandatory.
.PARAMETER rate
   the exchange rate. The default value is set to 1.37.
.EXAMPLE
   ConvertTo-Euro 100
   converts 100 dollars using the default exchange rate and positional parameters
.EXAMPLE
   ConvertTo-Euro 100 -rate 2.3
   converts 100 dollars using a custom exchange rate
#>

function ConvertTo-Euro {
    param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [Double]
    $dollar,

    $rate = 1.37,

    [switch]
    $pretty
    )
  begin {"starting..."}

  process {
        $result = $dollar * $rate

        if ($pretty) {
            '${0:0.00} equals EUR{1:0.00} at a rate of {2:0:0.00}' -f 
            $dollar, $result, $rate
        } else {
            $result
        }
    }
  
  end { "Done!" }

}

As you can see, your function code is now assigned to one of three special script blocks: begin, process, and end. Once you add one of these blocks, no code will exist outside of any one of these three blocks anymore.

Playing With Prompt Functions

PowerShell already contains some pre-defined functions. You can enumerate the special drive function if you would like to see all available functions:

Dir function: 

Many of these pre-defined functions perform important tasks in PowerShell. The most important place for customization is the function prompt, which is executed automatically once a command is done. It is responsible for displaying the PowerShell prompt. You can change your PowerShell prompt by overriding the function prompt. This will get you a colored prompt:

function prompt
{
    Write-Host ("PS " + $(get-location) +">") -nonewline -foregroundcolor Magenta
    " "
}

You can also insert information into the console screen buffer. This only works with true consoles so you cannot use this type of prompt in non-console editors, such as PowerShell ISE.

function prompt
{
    Write-Host ("PS " + $(get-location) +">") -nonewline -foregroundcolor Green 
    " "
    $winHeight = $Host.ui.rawui.WindowSize.Height
    $curPos = $Host.ui.rawui.CursorPosition
    $newPos = $curPos
    $newPos.X = 0
    $newPos.Y-=$winHeight
    $newPos.Y = [Math]::Max(0, $newPos.Y+1)
    $Host.ui.rawui.CursorPosition = $newPos
    Write-Host ("{0:D} {0:T}" -f (Get-Date)) -foregroundcolor Yellow
    $Host.ui.rawui.CursorPosition = $curPos
}

Another good place for additional information is the console window title bar. Here is a prompt that displays the current location in the title bar to save room inside the console and still display the current location:

function prompt { $host.ui.rawui.WindowTitle = (Get-Location) "PS> " }

And this prompt function changes colors based on your notebook battery status (provided you have a battery):

function prompt
{
   $charge = get-wmiobject Win32_Battery | 
    Measure-Object -property EstimatedChargeRemaining -average |
    Select-Object -expandProperty Average

   if ($charge -lt 25)
   {
      $color = "Red"
   } elseif ($charge -lt 50)
   {
      $color = "Yellow"
   } else
   {
      $color = "White"
   }
   $prompttext = "PS {0} ({1}%)>" f (get-location), $charge
   Write-Host $prompttext -nonewline -foregroundcolor $color
   " "
}

Summary

You can use functions to create your very own new cmdlets. In its most basic form, functions are called script blocks, which execute code whenever you enter the assigned name. That's what distinguishes functions from aliases. An alias serves solely as a replacement for another command name. As such, a function can execute whatever code you want.

PBy adding parameters, you can provide the user with the option to submit additional information to your function code. Parameters can do pretty much anything that cmdlet parameters can do. They can be mandatory, optional, have a default value, or a special data type. You can even add Switch parameters to your function.

If you want your function to work as part of a PowerShell pipeline, you will need to declare the parameter that should accept pipeline input from upstream cmdlets. You will also need to move the function code into a process block so it gets executed for each incoming result.

You can play with many more parameter attributes and declarations. Try this to get a complete overview:

Help advanced_parameter