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
- Defining Function Parameters
- Adding Mandatory Parameters
- Adding Switch Parameters
- Adding Help to your Functions
- Creating Pipeline-Aware Functions
- Playing With Prompt Functions
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