Understanding PowerShell Functions

Functions are reusable blocks of code that perform specific tasks in PowerShell. They help you organize code, reduce repetition, and create maintainable scripts. A well-designed function accepts inputs, processes them, and returns results while managing variable scope appropriately.

PowerShell functions follow a structured syntax that includes the function keyword, a name, parameters, and a script block containing the function’s logic.

Writing Functions in PowerShell: Complete Guide to Param, Return, and Scope

Basic Function Syntax

The simplest function in PowerShell contains just a name and a script block:

function Get-Greeting {
    "Hello, World!"
}

# Call the function
Get-Greeting

Output:

Hello, World!

PowerShell convention recommends using Verb-Noun naming for functions, following approved verbs from Get-Verb. This makes your functions consistent with native cmdlets.

Using the Param Block

The param block defines input parameters for your function. It must be the first executable statement inside the function body.

Simple Parameters

function Get-PersonInfo {
    param(
        $FirstName,
        $LastName,
        $Age
    )
    
    "Name: $FirstName $LastName"
    "Age: $Age"
}

# Call with parameters
Get-PersonInfo -FirstName "John" -LastName "Doe" -Age 30

Output:

Name: John Doe
Age: 30

Typed Parameters

Adding type constraints ensures parameters receive the correct data type and provides automatic validation:

function Add-Numbers {
    param(
        [int]$Number1,
        [int]$Number2
    )
    
    $Number1 + $Number2
}

# Valid call
Add-Numbers -Number1 10 -Number2 20

# This would throw an error
# Add-Numbers -Number1 "abc" -Number2 20

Output:

30

Mandatory Parameters

Use the [Parameter(Mandatory=$true)] attribute to require specific parameters:

function Get-FileSize {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    
    if (Test-Path $Path) {
        $file = Get-Item $Path
        "File size: $($file.Length) bytes"
    } else {
        "File not found: $Path"
    }
}

# PowerShell will prompt for Path if not provided
Get-FileSize -Path "C:\Windows\System32\notepad.exe"

Default Parameter Values

Assign default values to make parameters optional:

function New-LogEntry {
    param(
        [string]$Message,
        [string]$Level = "INFO",
        [string]$Timestamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
    )
    
    "[$Timestamp] [$Level] $Message"
}

# Using defaults
New-LogEntry -Message "Application started"

# Overriding defaults
New-LogEntry -Message "Critical error occurred" -Level "ERROR"

Output:

[2025-10-22 15:01:30] [INFO] Application started
[2025-10-22 15:01:30] [ERROR] Critical error occurred

Parameter Validation

PowerShell provides validation attributes to ensure parameters meet specific criteria:

function Set-ServerPort {
    param(
        [Parameter(Mandatory=$true)]
        [ValidateRange(1024, 65535)]
        [int]$Port,
        
        [ValidateSet("HTTP", "HTTPS", "FTP")]
        [string]$Protocol = "HTTP",
        
        [ValidateNotNullOrEmpty()]
        [string]$ServerName
    )
    
    "Configuring $ServerName on port $Port for $Protocol"
}

# Valid call
Set-ServerPort -Port 8080 -Protocol "HTTPS" -ServerName "webserver01"

# Invalid - port out of range
# Set-ServerPort -Port 80 -ServerName "webserver01"

Output:

Configuring webserver01 on port 8080 for HTTPS

Return Values and Output

PowerShell functions can return values in multiple ways, each with different behaviors and use cases.

Writing Functions in PowerShell: Complete Guide to Param, Return, and Scope

Pipeline Output

Any unassigned output in a function automatically goes to the pipeline:

function Get-MultipleValues {
    "First line"
    "Second line"
    "Third line"
}

$results = Get-MultipleValues
$results

Output:

First line
Second line
Third line

Using Return Statement

The return keyword exits the function immediately and sends a value to the pipeline:

function Test-NumberRange {
    param([int]$Number)
    
    if ($Number -lt 0) {
        return "Negative"
    }
    
    if ($Number -eq 0) {
        return "Zero"
    }
    
    return "Positive"
}

Test-NumberRange -Number -5
Test-NumberRange -Number 0
Test-NumberRange -Number 42

Output:

Negative
Zero
Positive

Returning Objects

Functions can return custom objects with multiple properties:

function Get-SystemInfo {
    [PSCustomObject]@{
        ComputerName = $env:COMPUTERNAME
        OSVersion = [System.Environment]::OSVersion.Version
        ProcessorCount = $env:NUMBER_OF_PROCESSORS
        UserName = $env:USERNAME
    }
}

$info = Get-SystemInfo
$info | Format-List

Output:

ComputerName   : DESKTOP-ABC123
OSVersion      : 10.0.19045.0
ProcessorCount : 8
UserName       : JohnDoe

Controlling Output with Write Cmdlets

Different Write cmdlets send output to specific streams:

function Invoke-ProcessWithLogging {
    param([string]$ProcessName)
    
    Write-Verbose "Starting process: $ProcessName" -Verbose
    Write-Host "Processing..." -ForegroundColor Cyan
    
    $result = [PSCustomObject]@{
        ProcessName = $ProcessName
        Status = "Completed"
        Timestamp = Get-Date
    }
    
    Write-Verbose "Process completed successfully" -Verbose
    
    # Only this goes to the pipeline
    $result
}

$output = Invoke-ProcessWithLogging -ProcessName "DataSync"
$output

Variable Scope in Functions

PowerShell uses different scopes to determine variable visibility and lifetime. Understanding scope is crucial for writing predictable functions.

Writing Functions in PowerShell: Complete Guide to Param, Return, and Scope

Local Scope

Variables created inside a function are local by default and don’t affect outside variables:

$globalVar = "I'm global"

function Test-LocalScope {
    $localVar = "I'm local"
    $globalVar = "I'm modified locally"
    
    "Inside function - localVar: $localVar"
    "Inside function - globalVar: $globalVar"
}

Test-LocalScope

"Outside function - globalVar: $globalVar"
# This would error - $localVar doesn't exist here
# "Outside function - localVar: $localVar"

Output:

Inside function - localVar: I'm local
Inside function - globalVar: I'm modified locally
Outside function - globalVar: I'm global

Script Scope

The $script: modifier accesses variables at the script level:

$script:counter = 0

function Increment-Counter {
    $script:counter++
    "Counter is now: $script:counter"
}

Increment-Counter
Increment-Counter
Increment-Counter

"Final counter value: $script:counter"

Output:

Counter is now: 1
Counter is now: 2
Counter is now: 3
Final counter value: 3

Global Scope

The $global: modifier creates or modifies variables accessible everywhere:

function Set-GlobalConfig {
    param([string]$ConfigValue)
    
    $global:AppConfig = $ConfigValue
    "Global configuration set"
}

Set-GlobalConfig -ConfigValue "Production"

# Accessible from anywhere
"Config value: $global:AppConfig"

Using Scope Modifiers

$global:environment = "Development"
$script:version = "1.0"

function Show-ScopeExample {
    $local:temp = "temporary"
    
    "Local variable: $temp"
    "Script variable: $script:version"
    "Global variable: $global:environment"
    
    # Modify parent scope
    $script:version = "1.1"
}

Show-ScopeExample
"Version after function: $script:version"

Output:

Local variable: temporary
Script variable: 1.0
Global variable: Development
Version after function: 1.1

Advanced Function Features

CmdletBinding Attribute

Adding [CmdletBinding()] transforms your function into an advanced function with additional capabilities:

function Get-ProcessMemory {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [string]$ProcessName = "*"
    )
    
    begin {
        Write-Verbose "Starting process memory check"
    }
    
    process {
        Get-Process -Name $ProcessName -ErrorAction SilentlyContinue | 
            Select-Object Name, 
                         @{Name='MemoryMB';Expression={[math]::Round($_.WorkingSet / 1MB, 2)}}
    }
    
    end {
        Write-Verbose "Process memory check completed"
    }
}

Get-ProcessMemory -ProcessName "powershell" -Verbose

Parameter Sets

Parameter sets allow different combinations of parameters:

function Get-UserData {
    [CmdletBinding(DefaultParameterSetName='ByName')]
    param(
        [Parameter(ParameterSetName='ByName', Mandatory=$true)]
        [string]$Name,
        
        [Parameter(ParameterSetName='ById', Mandatory=$true)]
        [int]$Id,
        
        [Parameter(ParameterSetName='All')]
        [switch]$All
    )
    
    switch ($PSCmdlet.ParameterSetName) {
        'ByName' { "Retrieving user: $Name" }
        'ById'   { "Retrieving user ID: $Id" }
        'All'    { "Retrieving all users" }
    }
}

Get-UserData -Name "JohnDoe"
Get-UserData -Id 12345
Get-UserData -All

Output:

Retrieving user: JohnDoe
Retrieving user ID: 12345
Retrieving all users

Pipeline Input

Functions can accept input from the pipeline using ValueFromPipeline:

function Convert-ToUpperCase {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [string]$InputString
    )
    
    process {
        $InputString.ToUpper()
    }
}

"hello", "world", "powershell" | Convert-ToUpperCase

Output:

HELLO
WORLD
POWERSHELL

Best Practices

Function Naming

Follow PowerShell naming conventions with approved verbs and singular nouns. Use Get-Verb to see approved verbs.

# Good
function Get-ServerStatus { }
function Set-Configuration { }
function Test-Connection { }

# Avoid
function ServerStatus { }
function ConfigSet { }
function CheckConnection { }

Comment-Based Help

Add help documentation directly in your functions:

function Get-DiskSpace {
    <#
    .SYNOPSIS
    Retrieves disk space information for specified drives.
    
    .DESCRIPTION
    This function queries the file system and returns available 
    and used disk space for one or more drives.
    
    .PARAMETER DriveLetter
    The drive letter to check (without colon). Defaults to C.
    
    .EXAMPLE
    Get-DiskSpace -DriveLetter "C"
    Gets disk space for C: drive.
    
    .EXAMPLE
    "C", "D", "E" | Get-DiskSpace
    Gets disk space for multiple drives via pipeline.
    #>
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true)]
        [string]$DriveLetter = "C"
    )
    
    process {
        $drive = Get-PSDrive -Name $DriveLetter -PSProvider FileSystem
        
        [PSCustomObject]@{
            Drive = $DriveLetter
            UsedGB = [math]::Round($drive.Used / 1GB, 2)
            FreeGB = [math]::Round($drive.Free / 1GB, 2)
            TotalGB = [math]::Round(($drive.Used + $drive.Free) / 1GB, 2)
        }
    }
}

# View help
Get-Help Get-DiskSpace -Full

Error Handling

Implement proper error handling with try-catch blocks:

function Get-FileContent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path
    )
    
    try {
        if (-not (Test-Path $Path)) {
            throw "File not found: $Path"
        }
        
        Get-Content -Path $Path -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to read file: $_"
        return $null
    }
}

Writing Functions in PowerShell: Complete Guide to Param, Return, and Scope

Output Type Declaration

Declare the output type for better IntelliSense and validation:

function Get-ComputerDetails {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param()
    
    [PSCustomObject]@{
        Name = $env:COMPUTERNAME
        Domain = $env:USERDOMAIN
        OS = [System.Environment]::OSVersion.VersionString
        PowerShellVersion = $PSVersionTable.PSVersion.ToString()
    }
}

Practical Examples

File Processing Function

function Process-LogFiles {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,
        
        [Parameter()]
        [string]$Pattern = "ERROR",
        
        [Parameter()]
        [int]$MaxResults = 100
    )
    
    begin {
        $results = @()
        Write-Verbose "Searching for pattern: $Pattern"
    }
    
    process {
        try {
            $files = Get-ChildItem -Path $Path -Filter "*.log" -File
            
            foreach ($file in $files) {
                $matches = Select-String -Path $file.FullName -Pattern $Pattern
                
                foreach ($match in $matches) {
                    $results += [PSCustomObject]@{
                        FileName = $file.Name
                        LineNumber = $match.LineNumber
                        Content = $match.Line.Trim()
                    }
                    
                    if ($results.Count -ge $MaxResults) {
                        break
                    }
                }
            }
        }
        catch {
            Write-Error "Error processing logs: $_"
        }
    }
    
    end {
        Write-Verbose "Found $($results.Count) matches"
        $results
    }
}

REST API Wrapper Function

function Invoke-ApiRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Endpoint,
        
        [Parameter()]
        [ValidateSet('GET', 'POST', 'PUT', 'DELETE')]
        [string]$Method = 'GET',
        
        [Parameter()]
        [hashtable]$Headers = @{},
        
        [Parameter()]
        [object]$Body
    )
    
    $script:apiCallCount++
    
    try {
        $params = @{
            Uri = $Endpoint
            Method = $Method
            Headers = $Headers
            ContentType = 'application/json'
        }
        
        if ($Body) {
            $params.Body = $Body | ConvertTo-Json
        }
        
        $response = Invoke-RestMethod @params
        
        [PSCustomObject]@{
            Success = $true
            Data = $response
            StatusCode = 200
            Timestamp = Get-Date
        }
    }
    catch {
        [PSCustomObject]@{
            Success = $false
            Data = $null
            StatusCode = $_.Exception.Response.StatusCode.Value__
            ErrorMessage = $_.Exception.Message
            Timestamp = Get-Date
        }
    }
}

Summary

Writing effective PowerShell functions requires understanding parameters, return mechanisms, and variable scope. Use the param block with appropriate validation attributes to create robust parameter definitions. Control output carefully using return statements and pipeline behavior. Manage variable scope with local, script, and global modifiers to prevent unintended side effects.

Advanced features like [CmdletBinding()], parameter sets, and pipeline input enable you to create professional-grade functions that integrate seamlessly with PowerShell’s ecosystem. Always include comment-based help, implement error handling, and follow naming conventions to make your functions maintainable and user-friendly.

Practice these concepts by building real-world functions that solve actual problems in your environment. Start simple with basic parameters and gradually incorporate advanced features as your requirements grow.