Functions
Note
Learn how to create reusable blocks of code with functions, parameters, and return values.
Overview
Functions are named blocks of code that you can call multiple times throughout your script. Instead of copying and pasting the same code, you put it in a function and call that function whenever you need it. This makes your code cleaner, easier to maintain, and reusable across different scripts.
Basic Syntax
# Simple function
function FunctionName {
# Code goes here
Write-Output "Function executed"
}
# Call the function
FunctionName
# Function with parameters
function Get-Greeting {
param(
[string]$Name
)
Write-Output "Hello, $Name!"
}
# Call with parameter
Get-Greeting -Name "Raymond"
Key Points
- Use approved verbs: Get, Set, New, Remove, Test, etc. (check with
Get-Verb) - Follow Verb-Noun naming:
Get-UserInfo, notGetInfoorUser-Get - Use
param()block for parameters - always at the top - Functions can return values (everything that outputs becomes the return value)
- Use comment-based help for documentation
Simple Functions
Function Without Parameters
function Show-SystemInfo {
$os = Get-CimInstance Win32_OperatingSystem
$computer = Get-CimInstance Win32_ComputerSystem
Write-Output "Computer: $($computer.Name)"
Write-Output "OS: $($os.Caption)"
Write-Output "Version: $($os.Version)"
Write-Output "Memory: $([math]::Round($computer.TotalPhysicalMemory / 1GB, 2)) GB"
}
# Call it
Show-SystemInfo
Function With Simple Parameter
function Test-IsEven {
param(
[int]$Number
)
if ($Number % 2 -eq 0) {
return $true
} else {
return $false
}
}
# Call it
Test-IsEven -Number 4 # Returns $true
Test-IsEven -Number 7 # Returns $false
Parameters
Parameter Block (param)
Always put param() first in your function:
Mandatory Parameters
function Get-UserInfo {
param(
[Parameter(Mandatory=$true)]
[string]$Username,
[Parameter(Mandatory=$false)] # Optional parameter
[string]$Domain = "DEFAULT" # Default value
)
Write-Output "User: $Username"
Write-Output "Domain: $Domain"
}
# Must provide Username, Domain is optional
Get-UserInfo -Username "jdoe"
Get-UserInfo -Username "jdoe" -Domain "CORPORATE"
Parameter Validation
function Set-ServiceStatus {
param(
[Parameter(Mandatory=$true)]
[ValidateSet("Running", "Stopped")] # Only these values allowed
[string]$Status,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()] # Cannot be empty or null
[string]$ServiceName,
[ValidateRange(1, 10)] # Number must be between 1 and 10
[int]$RetryCount = 3
)
Write-Output "Setting $ServiceName to $Status (retry up to $RetryCount times)"
}
# This works
Set-ServiceStatus -Status "Running" -ServiceName "Spooler"
# This fails validation
Set-ServiceStatus -Status "Paused" -ServiceName "Spooler" # ERROR: "Paused" not in ValidateSet
Switch Parameters (True/False Flags)
function Get-FileList {
param(
[string]$Path = "C:\Temp",
[switch]$IncludeHidden, # Switch parameter - just include or don't
[switch]$Recursive
)
$params = @{
Path = $Path
}
if ($IncludeHidden) {
$params.Force = $true
}
if ($Recursive) {
$params.Recurse = $true
}
Get-ChildItem @params
}
# Call with switches
Get-FileList -Path C:\Temp -IncludeHidden -Recursive
Get-FileList # Uses defaults, no switches
Return Values
Implicit Return (PowerShell Way)
Everything that outputs becomes the return value:
function Get-Double {
param([int]$Number)
$Number * 2 # This outputs, so it's returned
}
$result = Get-Double -Number 5
Write-Output $result # 10
Multiple Return Values
function Get-MinMax {
param([int[]]$Numbers)
$min = ($Numbers | Measure-Object -Minimum).Minimum
$max = ($Numbers | Measure-Object -Maximum).Maximum
# Return multiple values as an object
[PSCustomObject]@{
Minimum = $min
Maximum = $max
Range = $max - $min
}
}
$stats = Get-MinMax -Numbers @(5, 12, 3, 19, 7)
Write-Output "Min: $($stats.Minimum)"
Write-Output "Max: $($stats.Maximum)"
Write-Output "Range: $($stats.Range)"
Explicit Return
function Test-FileAge {
param(
[string]$Path,
[int]$DaysOld = 30
)
if (-not (Test-Path $Path)) {
Write-Warning "File not found: $Path"
return $false # Exit early
}
$file = Get-Item $Path
$age = (Get-Date) - $file.LastWriteTime
if ($age.TotalDays -gt $DaysOld) {
return $true
} else {
return $false
}
}
Write-Output vs Write-Host (IMPORTANT!)
Understanding output is critical for functions that work with the pipeline:
# BAD: Write-Host breaks the pipeline
function Get-BadData {
Write-Host "Here's your data: Important Value" # Goes to console only!
}
$result = Get-BadData
Write-Output $result # Empty! Can't capture Write-Host output
# GOOD: Use Write-Output or implicit output
function Get-GoodData {
Write-Output "Here's your data: Important Value" # Goes to pipeline
}
$result = Get-GoodData
Write-Output $result # Works! Returns the string
# EVEN BETTER: Implicit output (most PowerShell-like)
function Get-BestData {
"Here's your data: Important Value" # Automatically goes to pipeline
}
$result = Get-BestData # Works perfectly
When to use each: - Implicit output or Write-Output: For data you want returned/piped - Write-Host: ONLY for display text (progress messages, colored output for user) - Write-Verbose: For optional detailed messages (-Verbose flag) - Write-Information: For informational messages that can be captured
function Process-Data {
param([string]$Data)
# Display progress (not returned)
Write-Host "Processing $Data..." -ForegroundColor Yellow
# Return actual result (can be captured/piped)
[PSCustomObject]@{
Input = $Data
ProcessedAt = Get-Date
Success = $true
}
}
$result = Process-Data -Data "MyData" # Gets the object, not the Write-Host text
Return Objects, Not Formatted Text
PowerShell works best when functions return structured objects:
# BAD: Returns formatted string
function Get-UserInfoBad {
param([string]$Username)
$user = Get-ADUser -Identity $Username
"Name: $($user.Name), Email: $($user.EmailAddress)" # Just text
}
$info = Get-UserInfoBad -Username "jdoe"
# Can't do: $info.Name (it's just a string!)
# Can't export to CSV easily
# Can't filter or sort
# GOOD: Returns object
function Get-UserInfoGood {
param([string]$Username)
$user = Get-ADUser -Identity $Username
[PSCustomObject]@{
Name = $user.Name
Email = $user.EmailAddress
Department = $user.Department
Enabled = $user.Enabled
}
}
$info = Get-UserInfoGood -Username "jdoe"
# Now you can:
$info.Name # Access properties
$info | Export-Csv users.csv # Export to CSV
$info | Where-Object {$_.Enabled -eq $true} # Filter
$info | Select-Object Name, Email # Select columns
Why this matters: - Objects can be piped to other cmdlets - Properties can be accessed directly - Can be exported to CSV, JSON, XML - Can be filtered, sorted, grouped - Follows PowerShell conventions
Advanced Functions
CmdletBinding (Makes it a "Real" Cmdlet)
function Get-ProcessInfo {
[CmdletBinding()] # Adds common parameters and features
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]$ProcessName
)
begin {
# Runs once before processing pipeline input
Write-Verbose "Starting process lookup..."
}
process {
# Runs once for each pipeline input
$proc = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
if ($proc) {
[PSCustomObject]@{
Name = $proc.Name
ID = $proc.Id
MemoryMB = [math]::Round($proc.WS / 1MB, 2)
CPU = $proc.CPU
}
}
}
end {
# Runs once after all pipeline input processed
Write-Verbose "Process lookup complete"
}
}
# Now you get -Verbose, -Debug, etc. for free
Get-ProcessInfo -ProcessName "powershell" -Verbose
# Works with pipeline
"powershell", "code", "chrome" | Get-ProcessInfo
Comment-Based Help
function Get-DiskSpace {
<#
.SYNOPSIS
Gets free disk space for specified drive.
.DESCRIPTION
Retrieves the free space for a drive and returns it in gigabytes.
Optionally warns if space is below a threshold.
.PARAMETER DriveLetter
The drive letter to check (e.g., 'C', 'D').
.PARAMETER WarnThresholdGB
Warn if free space is below this many gigabytes.
.EXAMPLE
Get-DiskSpace -DriveLetter C
Returns free space on C: drive.
.EXAMPLE
Get-DiskSpace -DriveLetter C -WarnThresholdGB 50
Warns if C: has less than 50GB free.
.NOTES
Author: Your Name
Date: 2025-11-29
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[ValidatePattern('^[A-Z]$')]
[string]$DriveLetter,
[int]$WarnThresholdGB = 0
)
$drive = Get-PSDrive -Name $DriveLetter -ErrorAction Stop
$freeGB = [math]::Round($drive.Free / 1GB, 2)
if ($WarnThresholdGB -gt 0 -and $freeGB -lt $WarnThresholdGB) {
Write-Warning "Low disk space on ${DriveLetter}: ${freeGB}GB free"
}
[PSCustomObject]@{
Drive = $DriveLetter
FreeSpaceGB = $freeGB
TotalSpaceGB = [math]::Round($drive.Used / 1GB + $freeGB, 2)
}
}
# View help
Get-Help Get-DiskSpace -Full
Get-Help Get-DiskSpace -Examples
Real-World Examples
Example: Logging Function
Scenario: Create a reusable logging function for all your scripts
function Write-Log {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[string]$Message,
[ValidateSet("INFO", "WARNING", "ERROR")]
[string]$Level = "INFO",
[string]$LogPath = "C:\Logs\script.log"
)
process {
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Write to console with color
$color = switch ($Level) {
"INFO" { "White" }
"WARNING" { "Yellow" }
"ERROR" { "Red" }
}
Write-Host $logEntry -ForegroundColor $color
# Write to file
Add-Content -Path $LogPath -Value $logEntry
}
}
# Usage
Write-Log "Script started"
Write-Log "Disk space low" -Level WARNING
Write-Log "Failed to connect" -Level ERROR
# Pipeline usage
"Process 1", "Process 2" | Write-Log
Example: File Cleanup Function
Scenario: Clean up old files with safety checks and logging
function Remove-OldFiles {
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[Parameter(Mandatory=$true)]
[string]$Path,
[Parameter(Mandatory=$true)]
[int]$DaysOld,
[string]$Filter = "*.*",
[switch]$Recurse
)
begin {
if (-not (Test-Path $Path)) {
throw "Path not found: $Path"
}
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
Write-Verbose "Removing files older than: $cutoffDate"
}
process {
$params = @{
Path = $Path
Filter = $Filter
File = $true
}
if ($Recurse) {
$params.Recurse = $true
}
$oldFiles = Get-ChildItem @params |
Where-Object { $_.LastWriteTime -lt $cutoffDate }
foreach ($file in $oldFiles) {
if ($PSCmdlet.ShouldProcess($file.FullName, "Delete")) {
try {
Remove-Item -Path $file.FullName -Force
Write-Verbose "Deleted: $($file.Name)"
}
catch {
Write-Error "Failed to delete $($file.Name): $_"
}
}
}
}
end {
Write-Verbose "Cleanup complete"
}
}
# Usage
Remove-OldFiles -Path C:\Logs -DaysOld 30 -Filter "*.log" -Verbose
# WhatIf mode (doesn't actually delete)
Remove-OldFiles -Path C:\Logs -DaysOld 30 -WhatIf
Example: Retry Logic Function
Scenario: Reusable function to retry operations with exponential backoff
function Invoke-WithRetry {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
[scriptblock]$ScriptBlock,
[int]$MaxRetries = 3,
[int]$InitialDelaySeconds = 2,
[switch]$ExponentialBackoff
)
$attempt = 0
$success = $false
while ($attempt -lt $MaxRetries -and -not $success) {
$attempt++
try {
Write-Verbose "Attempt $attempt of $MaxRetries..."
# Execute the script block
$result = & $ScriptBlock
$success = $true
return $result
}
catch {
Write-Warning "Attempt $attempt failed: $($_.Exception.Message)"
if ($attempt -lt $MaxRetries) {
if ($ExponentialBackoff) {
$delay = $InitialDelaySeconds * [math]::Pow(2, $attempt - 1)
} else {
$delay = $InitialDelaySeconds
}
Write-Verbose "Waiting $delay seconds before retry..."
Start-Sleep -Seconds $delay
}
}
}
if (-not $success) {
throw "Operation failed after $MaxRetries attempts"
}
}
# Usage
$result = Invoke-WithRetry -ScriptBlock {
Test-Connection -ComputerName "Server01" -Count 1 -ErrorAction Stop
} -MaxRetries 5 -ExponentialBackoff -Verbose
Scope and Variables
# Variables in functions are local by default
function Test-Scope {
$localVar = "I'm local to the function"
Write-Output $localVar
}
Test-Scope # Works
Write-Output $localVar # Empty - variable doesn't exist outside function
# Access global variables
$global:sharedValue = "Available everywhere"
function Use-Global {
Write-Output $global:sharedValue # Can read global
$global:sharedValue = "Modified" # Can modify global (use sparingly!)
}
# Script scope (shared across functions in same script)
$script:scriptLevel = "Shared in script"
function One {
Write-Output $script:scriptLevel # Can access
$script:scriptLevel = "Changed"
}
function Two {
Write-Output $script:scriptLevel # Sees the change
}
Tips & Tricks
Use Approved Verbs
Use Splatting for Readable Parameter Passing
Return Objects, Not Formatted Text
# BAD: Returns formatted text
function Get-UserInfo {
$user = Get-ADUser -Identity "jdoe"
Write-Output "Name: $($user.Name), Email: $($user.Email)"
}
# GOOD: Returns object that can be used
function Get-UserInfo {
$user = Get-ADUser -Identity "jdoe"
[PSCustomObject]@{
Name = $user.Name
Email = $user.Email
}
}
# Now you can: pipe it, select properties, export to CSV, etc.
Get-UserInfo | Export-Csv users.csv
Don't Use Write-Host for Output
# BAD: Write-Host breaks the pipeline
function Get-Data {
Write-Host "Here's your data" # This can't be captured or piped!
}
# GOOD: Use Write-Output (or just output directly)
function Get-Data {
Write-Output "Here's your data" # Can be captured and piped
# Or just:
"Here's your data" # Implicit output
}
# Write-Host is only for display (like progress messages)
# Use Write-Verbose or Write-Information for informational output
Watch Out for Unwanted Output
function Add-ToList {
$list = [System.Collections.ArrayList]@()
$list.Add("Item1") # Returns the index added (0)
$list.Add("Item2") # Returns 1
return $list # Returns: 0, 1, ArrayList
}
# FIX: Suppress unwanted output
function Add-ToList {
$list = [System.Collections.ArrayList]@()
$list.Add("Item1") | Out-Null # Suppress index output
$list.Add("Item2") | Out-Null
return $list # Returns only ArrayList
}
Parameter Defaults and Mandatory Don't Mix
# This makes no sense - it's mandatory but has a default?
param(
[Parameter(Mandatory=$true)]
[string]$Name = "Default" # Contradiction!
)
# Pick one or the other:
param(
[Parameter(Mandatory=$true)] # Required, no default
[string]$Name
)
# OR
param(
[string]$Name = "Default" # Optional with default
)
Related Topics
- Variables & Data Types - Understanding function parameters and return values
- If Statements & Conditional Logic - Using conditionals in functions
- Loops - Combining loops with functions
- Error Handling - Try/catch in functions
- Script Scope - Understanding variable scope