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
Testing Your Functions
Before saving a function to a script file, you should test it to make sure it works correctly. PowerShell makes this easy by allowing you to define functions directly in the console.
Copy-Paste Testing (Quick & Easy)
The fastest way to test a function:
- Write your function in a script editor (VS Code, PowerShell ISE, or notepad)
- Select the entire function definition (from
functionto the closing}) - Copy it (Ctrl+C)
- Paste it into PowerShell console and press Enter
- Now call the function to test it
# 1. Copy this entire function
function Get-TimeGreeting {
param([string]$Name)
$hour = (Get-Date).Hour
$timeOfDay = if ($hour -lt 12) { "Morning" }
elseif ($hour -lt 17) { "Afternoon" }
else { "Evening" }
"Good $timeOfDay, $Name!"
}
# 2. Paste it into PowerShell console
# 3. Now test it:
Get-TimeGreeting -Name "Raymond"
Why This Works
When you paste a function definition into PowerShell, it loads the function into your current session's memory. The function stays available until you close PowerShell or redefine it.
Testing in VS Code PowerShell Extension
If you're using VS Code with the PowerShell extension:
- Write your function in a
.ps1file - Select the function definition
- Press
F8(or right-click → "Run Selection") - The function loads into the integrated terminal
- Call it in the terminal to test
# In your .ps1 file
function Test-IsEven {
param([int]$Number)
($Number % 2) -eq 0
}
# Select the function, press F8
# Then in terminal:
Test-IsEven -Number 10 # $true
Test-IsEven -Number 7 # $false
Testing in PowerShell ISE
If you're using PowerShell ISE:
- Write your function in the script pane (top)
- Select the entire function
- Press
F8(or click "Run Selection") - Test it in the console pane (bottom)
Dot-Source Testing (For Script Files)
If your function is saved in a .ps1 file:
# Your function is in: C:\Scripts\MyFunctions.ps1
# Load all functions from that file into your session:
. C:\Scripts\MyFunctions.ps1
# Now you can call any function from that file
Get-TimeGreeting -Name "Raymond"
The . (dot-space) at the start is called "dot-sourcing" - it runs the script in your current scope, making all functions available.
Iterative Testing Workflow
The typical development cycle:
# 1. Write initial function (in VS Code, ISE, or editor)
function Get-FileSize {
param([string]$Path)
$file = Get-Item $Path
$file.Length
}
# 2. Paste/run it in PowerShell (F8 in VS Code)
# 3. Test it
Get-FileSize -Path "C:\Windows\notepad.exe" # Returns bytes
# 4. Realize you want GB instead - modify the function
function Get-FileSize {
param([string]$Path)
$file = Get-Item $Path
[math]::Round($file.Length / 1GB, 2) # Changed to GB
}
# 5. Run the modified function again (F8)
# This overwrites the previous version in memory
# 6. Test again
Get-FileSize -Path "C:\Windows\notepad.exe" # Returns GB
# 7. Repeat until satisfied
Testing with Different Inputs
Always test with various inputs:
# Define your function
function Get-DayOfWeek {
param([datetime]$Date = (Get-Date))
$Date.DayOfWeek
}
# Test with default (no parameter)
Get-DayOfWeek
# Test with specific date
Get-DayOfWeek -Date "2025-12-25"
# Test with different formats
Get-DayOfWeek -Date "January 1, 2025"
# Test edge cases
Get-DayOfWeek -Date "1900-01-01" # Very old date
Checking If a Function Exists
Useful when testing to see what's loaded:
# Check if a function is loaded in your session
Get-Command Get-TimeGreeting
# List all loaded functions
Get-Command -CommandType Function
# Remove a function from session (to start fresh)
Remove-Item Function:\Get-TimeGreeting
Test Parameters and Validation
# Define function with validation
function Set-ServiceStatus {
param(
[ValidateSet("Running", "Stopped")]
[string]$Status
)
"Setting status to: $Status"
}
# Load it (paste or F8)
# Test valid input
Set-ServiceStatus -Status "Running" # Works
# Test invalid input (should error)
Set-ServiceStatus -Status "Paused" # ERROR - not in ValidateSet
# Test mandatory parameters
function Get-User {
param(
[Parameter(Mandatory=$true)]
[string]$Username
)
"User: $Username"
}
# Load and call without parameter (should prompt)
Get-User # PowerShell prompts: "Username:"
Functions Persist in Session
Once you paste/load a function into PowerShell, it stays in memory until:
- You close PowerShell
- You remove it with
Remove-Item Function:\FunctionName - You redefine it (paste a new version)
This is great for testing but can be confusing if you forget which version is loaded!
Quick Function Reload
If you're repeatedly testing the same function: 1. Keep your editor and PowerShell side-by-side 2. Modify function in editor 3. Select all → F8 (or copy-paste) 4. Test immediately in console 5. Repeat
This is much faster than saving to a file and dot-sourcing each time.
Testing Output Types
# Function that returns an object
function Get-SystemInfo {
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
CurrentUser = $env:USERNAME
PowerShellVersion = $PSVersionTable.PSVersion.ToString()
}
}
# Load and test
$info = Get-SystemInfo
# Verify it's an object (not just text)
$info.GetType() # Should be PSCustomObject
$info.ComputerName # Should access property
$info | Get-Member # Shows all properties and methods
$info | ConvertTo-Json # Should serialize properly
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 (
-Verboseflag) - 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
Functions can include built-in help documentation using special comment keywords. This help integrates with PowerShell's Get-Help system, making your functions self-documenting and easy to use.
Quick Example
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. Default is 0 (no warning).
.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
Version: 1.0.0
#>
[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)
}
}
Using Get-Help
Once you've defined a function with help, you can view it using Get-Help:
# View basic help
Get-Help Get-DiskSpace
# View detailed help
Get-Help Get-DiskSpace -Full
# View just examples
Get-Help Get-DiskSpace -Examples
# View specific parameter help
Get-Help Get-DiskSpace -Parameter DriveLetter
Placement in Functions
There are three ways to place help in a function:
Option 1: Inside function, at top (Most Common)
Option 2: Before the function keyword
Option 3: Inside function, at end (Rare)
Recommendation: Use Option 1 - it's the most common and keeps help close to parameters.
Complete Help Reference
For comprehensive documentation on all help keywords (.SYNOPSIS, .DESCRIPTION, .PARAMETER, .EXAMPLE, .NOTES, .LINK, .INPUTS, .OUTPUTS, etc.) and detailed examples, see Comment-Based Help.
Best Practices for Function Help
Always include these minimum keywords:
.SYNOPSIS- One-line description.DESCRIPTION- Detailed explanation.PARAMETER- Document every parameter.EXAMPLE- At least one working example
Tips for good help:
# Good .SYNOPSIS (concise, clear)
.SYNOPSIS
Gets disk space information for specified drives
# Bad .SYNOPSIS (too vague)
.SYNOPSIS
Gets information
# Good .PARAMETER (explains purpose, notes defaults)
.PARAMETER WarnThresholdGB
Warn if free space is below this many gigabytes. Default is 0 (no warning).
# Bad .PARAMETER (just repeats the name)
.PARAMETER WarnThresholdGB
The warn threshold in GB
# Good .EXAMPLE (shows real command + explains what it does)
.EXAMPLE
Get-DiskSpace -DriveLetter C -WarnThresholdGB 50
Checks C: drive and warns if less than 50GB free.
# Bad .EXAMPLE (no explanation)
.EXAMPLE
Get-DiskSpace -DriveLetter C
Additional recommendations:
- Include multiple
.EXAMPLEentries for different use cases - Use
.NOTESfor author, version, and requirements - Use
.LINKto reference related cmdlets or documentation - Keep examples realistic and copy-pasteable
- Update help when you modify the function
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