Skip to content

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, not GetInfo or User-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:

function Do-Something {
    param(
        [string]$Name,
        [int]$Age,
        [bool]$IsActive
    )

    # Function code here
}

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

# Check approved verbs
Get-Verb | Out-GridView

# Common approved verbs:
Get, Set, New, Remove, Add, Clear, Test, Invoke, Start, Stop

# DON'T use:
function Fetch-Data { }   # Use Get-Data
function Create-File { }  # Use New-File
function Delete-Item { }  # Use Remove-Item

Use Splatting for Readable Parameter Passing

function Get-FileList {
    param($Path, $Filter, $Recurse)
    Get-ChildItem @PSBoundParameters
}

# Or build hash table
$params = @{
    Path = "C:\Temp"
    Filter = "*.log"
    Recurse = $true
}

Get-ChildItem @params  # Clean and readable

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
)

Additional Resources