Functions¶
Functions let you package reusable logic, give it a name, and call it with parameters. They are the primary unit of reusable code in PowerShell.
Basic Function¶
Functions with Parameters¶
function Get-Greeting {
param (
[string] $Name,
[string] $Title = "Dr." # default value
)
"Hello, $Title $Name!"
}
Get-Greeting -Name "Smith" # Hello, Dr. Smith!
Get-Greeting -Name "Jones" -Title "Mr" # Hello, Mr Jones!
Get-Greeting "Alice" # positional — Hello, Dr. Alice!
Return Values¶
Functions return the last expression evaluated (or use return explicitly):
function Add-Numbers {
param ([int]$A, [int]$B)
$A + $B # implicitly returned
}
$sum = Add-Numbers 3 7 # $sum = 10
function Get-Status {
param ([string]$Name)
$svc = Get-Service -Name $Name -ErrorAction SilentlyContinue
if ($null -eq $svc) { return "Not found" }
return $svc.Status
}
All output is returned
In PowerShell, every uncaptured expression in a function becomes part of the return value — not just the return statement. Assign intermediate results to $null or use [void] to suppress unintended output:
Pipeline Input¶
Make a function accept pipeline input with [Parameter(ValueFromPipeline)]:
function Show-Name {
param (
[Parameter(ValueFromPipeline)]
[string] $Name
)
process {
"Name: $Name"
}
}
"Alice","Bob","Charlie" | Show-Name
The process block runs once per pipeline object. Use begin and end for setup/teardown:
function Count-Items {
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
begin { $count = 0 }
process { $count++ }
end { "Total: $count items" }
}
1..100 | Count-Items
Scope¶
Variables created inside a function are local by default:
$x = "outer"
function Test-Scope {
$x = "inner"
"Inside: $x"
}
Test-Scope # Inside: inner
"Outside: $x" # Outside: outer
Use scope modifiers to write to outer scopes (use sparingly — prefer returning values):
Splatting Parameters¶
Use a hashtable to pass named parameters — useful for building dynamic parameter sets:
$params = @{
Path = "C:\Windows"
Filter = "*.exe"
Recurse = $true
ErrorAction = "SilentlyContinue"
}
Get-ChildItem @params
You can also splat arrays for positional parameters:
CmdletBinding and Common Parameters¶
Add [CmdletBinding()] to make your function behave like a real cmdlet with all the common parameters (-Verbose, -Debug, -ErrorAction, -WhatIf, -Confirm, etc.):
function Remove-OldLogs {
[CmdletBinding(SupportsShouldProcess)]
param (
[string] $Path = ".\logs",
[int] $DaysOld = 30
)
$cutoff = (Get-Date).AddDays(-$DaysOld)
Get-ChildItem $Path -Filter *.log |
Where-Object { $_.LastWriteTime -lt $cutoff } |
ForEach-Object {
if ($PSCmdlet.ShouldProcess($_.FullName, "Delete")) {
Remove-Item $_.FullName
Write-Verbose "Deleted $($_.FullName)"
}
}
}
Remove-OldLogs -Path C:\Logs -DaysOld 7 -WhatIf
Remove-OldLogs -Path C:\Logs -DaysOld 7 -Verbose
See Advanced Functions for the full details.
Anonymous Script Blocks¶
Script blocks { } are unnamed functions. Use them with Invoke-Command, &, and pipeline cmdlets:
$double = { param($n) $n * 2 }
& $double 5 # 10
$greet = { "Hello, $_!" }
"Alice","Bob" | ForEach-Object $greet
Best Practices¶
- Use Verb-Noun names from the approved verb list (
Get-Verb) - Always add type constraints to parameters — it catches bugs early
- Use
Write-Verbosefor diagnostic messages — notWrite-Host - Return objects, not formatted strings so callers can further process them
- Support
-WhatIfand-Confirmfor functions that make changes - Add comment-based help so
Get-Helpworks on your function
function Get-ActiveUser {
<#
.SYNOPSIS
Returns users who logged in within the last N days.
.PARAMETER Days
Number of days to look back (default: 30).
.EXAMPLE
Get-ActiveUser -Days 7
#>
[CmdletBinding()]
param (
[int] $Days = 30
)
$cutoff = (Get-Date).AddDays(-$Days)
Get-LocalUser | Where-Object {
$_.LastLogon -gt $cutoff -and $_.Enabled
}
}