Skip to content

Common PowerShell Patterns

Note

Reusable code patterns and templates for common PowerShell tasks - copy, adapt, and use in your scripts.

Overview

These are battle-tested patterns you'll use repeatedly in PowerShell. Each pattern solves a specific problem and can be adapted to your needs. Think of them as templates you can customize for your situation.


File System Patterns

Pattern: Find Files by Multiple Criteria

When to use: Finding files that match complex conditions

# Find large, old files
$targetPath = "C:\Data"
$sizeLimitMB = 100
$daysOld = 30
$cutoffDate = (Get-Date).AddDays(-$daysOld)

$files = Get-ChildItem -Path $targetPath -Recurse -File |
    Where-Object {
        ($_.Length -gt ($sizeLimitMB * 1MB)) -and
        ($_.LastWriteTime -lt $cutoffDate)
    }

# Report findings
Write-Output "Found $($files.Count) large, old files"
$totalSizeGB = ($files | Measure-Object -Property Length -Sum).Sum / 1GB
Write-Output "Total size: $([math]::Round($totalSizeGB, 2)) GB"

Pattern: Process Files in Batches

When to use: Processing large numbers of files safely

$sourceFolder = "C:\Files"
$batchSize = 100
$files = Get-ChildItem -Path $sourceFolder -File

for ($i = 0; $i -lt $files.Count; $i += $batchSize) {
    $batch = $files[$i..([Math]::Min($i + $batchSize - 1, $files.Count - 1))]

    Write-Output "Processing batch $([Math]::Floor($i / $batchSize) + 1)"

    foreach ($file in $batch) {
        # Process each file
        Write-Output "  Processing: $($file.Name)"
    }

    # Optional: Pause between batches
    Start-Sleep -Seconds 2
}

Pattern: Backup Files Before Modifying

When to use: Making changes to files with safety net

$file = "C:\Config\settings.json"
$backupPath = "$file.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')"

try {
    # Create backup
    Copy-Item -Path $file -Destination $backupPath -ErrorAction Stop
    Write-Output "Backup created: $backupPath"

    # Make changes
    $content = Get-Content -Path $file
    $content = $content -replace "OldValue", "NewValue"
    $content | Set-Content -Path $file

    Write-Output "File updated successfully"

} catch {
    # If anything fails, restore backup
    if (Test-Path $backupPath) {
        Copy-Item -Path $backupPath -Destination $file -Force
        Write-Warning "Changes failed, backup restored"
    }
    throw $_
}

Data Collection & Reporting Patterns

Pattern: Build a Report Object

When to use: Creating structured reports from various sources

$report = [PSCustomObject]@{
    Timestamp = Get-Date
    ComputerName = $env:COMPUTERNAME
    DiskSpaceGB = [math]::Round((Get-PSDrive C).Free / 1GB, 2)
    ProcessCount = (Get-Process).Count
    RunningServices = (Get-Service | Where-Object {$_.Status -eq "Running"}).Count
    UptimeHours = [math]::Round(((Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime).TotalHours, 2)
}

# Display report
$report | Format-List

# Export to CSV
$report | Export-Csv -Path "C:\Reports\system-status.csv" -Append -NoTypeInformation

# Export to JSON
$report | ConvertTo-Json | Out-File "C:\Reports\system-status.json"

Pattern: Aggregate Data from Multiple Sources

When to use: Combining data from different commands or systems

$results = @()

$computers = @("Server01", "Server02", "Server03")

foreach ($computer in $computers) {
    try {
        $os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $computer
        $disk = Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $computer -Filter "DeviceID='C:'"

        $results += [PSCustomObject]@{
            Computer = $computer
            Status = "Online"
            OS = $os.Caption
            FreeSpaceGB = [math]::Round($disk.FreeSpace / 1GB, 2)
            TotalSpaceGB = [math]::Round($disk.Size / 1GB, 2)
            LastBoot = $os.LastBootUpTime
        }
    } catch {
        $results += [PSCustomObject]@{
            Computer = $computer
            Status = "Offline"
            OS = $null
            FreeSpaceGB = $null
            TotalSpaceGB = $null
            LastBoot = $null
        }
    }
}

# Display results
$results | Format-Table -AutoSize

Error Handling Patterns

Pattern: Retry Logic with Exponential Backoff

When to use: Operations that might fail temporarily (network, locks, etc.)

function Invoke-WithRetry {
    param(
        [ScriptBlock]$ScriptBlock,
        [int]$MaxRetries = 3,
        [int]$InitialDelaySeconds = 2
    )

    $attempt = 0
    $delay = $InitialDelaySeconds

    while ($attempt -lt $MaxRetries) {
        $attempt++

        try {
            & $ScriptBlock
            Write-Output "Success on attempt $attempt"
            return $true
        } catch {
            Write-Warning "Attempt $attempt failed: $_"

            if ($attempt -lt $MaxRetries) {
                Write-Output "Waiting $delay seconds before retry..."
                Start-Sleep -Seconds $delay
                $delay *= 2  # Exponential backoff
            } else {
                Write-Error "All $MaxRetries attempts failed"
                throw $_
            }
        }
    }
    return $false
}

# Usage
Invoke-WithRetry -ScriptBlock {
    $response = Invoke-RestMethod -Uri "https://api.example.com/data"
    $response
}

Pattern: Collect Errors Without Stopping

When to use: Processing multiple items where failures shouldn't stop the whole operation

$files = Get-ChildItem -Path "C:\Files" -File
$successCount = 0
$errors = @()

foreach ($file in $files) {
    try {
        # Process file
        $content = Get-Content -Path $file.FullName -ErrorAction Stop
        # Do something with content
        $successCount++

    } catch {
        # Log error but continue
        $errors += [PSCustomObject]@{
            File = $file.Name
            Error = $_.Exception.Message
            Timestamp = Get-Date
        }
        Write-Warning "Failed to process $($file.Name): $_"
    }
}

# Final report
Write-Output "`nProcessing complete:"
Write-Output "  Successful: $successCount"
Write-Output "  Failed: $($errors.Count)"

if ($errors.Count -gt 0) {
    $errors | Export-Csv -Path "C:\Logs\errors.csv" -NoTypeInformation
    Write-Output "  Error log: C:\Logs\errors.csv"
}

Input Validation Patterns

Pattern: Validate and Sanitize User Input

When to use: Accepting input from users or external sources

function Get-ValidatedPath {
    param([string]$Prompt)

    do {
        $path = Read-Host $Prompt

        # Check if path is empty
        if ([string]::IsNullOrWhiteSpace($path)) {
            Write-Warning "Path cannot be empty"
            continue
        }

        # Check if path exists
        if (-not (Test-Path $path)) {
            Write-Warning "Path does not exist: $path"
            $retry = Read-Host "Try again? (Y/N)"
            if ($retry -ne "Y") { return $null }
            continue
        }

        # Path is valid
        return $path

    } while ($true)
}

# Usage
$userPath = Get-ValidatedPath -Prompt "Enter folder path"
if ($null -ne $userPath) {
    Write-Output "Using path: $userPath"
}

Pattern: Validate Multiple Conditions

When to use: Ensuring data meets complex requirements

function Test-FileValid {
    param(
        [string]$FilePath,
        [int]$MaxSizeMB = 100,
        [string[]]$AllowedExtensions = @(".txt", ".csv", ".json")
    )

    # Check existence
    if (-not (Test-Path $FilePath)) {
        throw "File not found: $FilePath"
    }

    $file = Get-Item $FilePath

    # Check size
    $sizeMB = $file.Length / 1MB
    if ($sizeMB -gt $MaxSizeMB) {
        throw "File too large: $([math]::Round($sizeMB, 2)) MB (max: $MaxSizeMB MB)"
    }

    # Check extension
    if ($file.Extension -notin $AllowedExtensions) {
        throw "Invalid file type: $($file.Extension) (allowed: $($AllowedExtensions -join ', '))"
    }

    # Check not empty
    if ($file.Length -eq 0) {
        throw "File is empty"
    }

    return $true
}

# Usage
try {
    Test-FileValid -FilePath "C:\Data\file.txt"
    Write-Output "File is valid, proceeding..."
} catch {
    Write-Error "Validation failed: $_"
    exit 1
}

Configuration Patterns

Pattern: Load Configuration from JSON

When to use: Storing script settings in a config file

# config.json file:
# {
#     "LogPath": "C:\\Logs\\app.log",
#     "MaxRetries": 3,
#     "EmailRecipients": ["admin@company.com", "ops@company.com"]
# }

function Get-Config {
    param([string]$ConfigPath = ".\config.json")

    if (-not (Test-Path $ConfigPath)) {
        throw "Config file not found: $ConfigPath"
    }

    try {
        $config = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
        return $config
    } catch {
        throw "Failed to parse config file: $_"
    }
}

# Usage
$config = Get-Config
Write-Output "Log path: $($config.LogPath)"
Write-Output "Max retries: $($config.MaxRetries)"

Pattern: Environment-Based Configuration

When to use: Different settings for Dev/Test/Prod environments

function Get-EnvironmentConfig {
    param(
        [ValidateSet("Development", "Testing", "Production")]
        [string]$Environment = "Development"
    )

    $configs = @{
        Development = @{
            DatabaseServer = "localhost"
            LogLevel = "Verbose"
            EnableDebug = $true
        }
        Testing = @{
            DatabaseServer = "test-sql01"
            LogLevel = "Information"
            EnableDebug = $true
        }
        Production = @{
            DatabaseServer = "prod-sql01"
            LogLevel = "Warning"
            EnableDebug = $false
        }
    }

    return $configs[$Environment]
}

# Usage
$env = "Production"
$config = Get-EnvironmentConfig -Environment $env
Write-Output "Connecting to: $($config.DatabaseServer)"

Logging Patterns

Pattern: Simple File Logging

When to use: Recording script activities for later review

function Write-Log {
    param(
        [string]$Message,
        [ValidateSet("INFO", "WARNING", "ERROR")]
        [string]$Level = "INFO",
        [string]$LogPath = "C:\Logs\script.log"
    )

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] [$Level] $Message"

    # Write to console with color
    switch ($Level) {
        "INFO"    { Write-Host $logEntry -ForegroundColor Green }
        "WARNING" { Write-Host $logEntry -ForegroundColor Yellow }
        "ERROR"   { Write-Host $logEntry -ForegroundColor Red }
    }

    # Append to log file
    Add-Content -Path $LogPath -Value $logEntry
}

# Usage
Write-Log "Script started" -Level INFO
Write-Log "Processing 100 files" -Level INFO
Write-Log "Connection timeout, retrying..." -Level WARNING
Write-Log "Critical error occurred" -Level ERROR

Pattern: Log with Rotation

When to use: Preventing log files from growing too large

function Write-RotatingLog {
    param(
        [string]$Message,
        [string]$LogPath = "C:\Logs\app.log",
        [int]$MaxSizeMB = 10
    )

    # Check if log file exists and size
    if (Test-Path $LogPath) {
        $logFile = Get-Item $LogPath
        $sizeMB = $logFile.Length / 1MB

        # Rotate if too large
        if ($sizeMB -gt $MaxSizeMB) {
            $archivePath = "$LogPath.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
            Move-Item -Path $LogPath -Destination $archivePath
            Write-Output "Log rotated to: $archivePath"
        }
    }

    # Write log entry
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logEntry = "[$timestamp] $Message"
    Add-Content -Path $LogPath -Value $logEntry
}

Progress Reporting Patterns

Pattern: Progress Bar for Long Operations

When to use: Processing many items and want to show progress

$files = Get-ChildItem -Path "C:\Data" -Recurse -File
$totalCount = $files.Count
$currentCount = 0

foreach ($file in $files) {
    $currentCount++
    $percentComplete = ($currentCount / $totalCount) * 100

    Write-Progress -Activity "Processing files" `
                   -Status "File $currentCount of $totalCount : $($file.Name)" `
                   -PercentComplete $percentComplete

    # Process file
    Start-Sleep -Milliseconds 100  # Simulate work

}

Write-Progress -Activity "Processing files" -Completed
Write-Output "Processing complete!"

Pattern: Estimated Time Remaining

When to use: Long-running operations where users want to know how much longer

$items = 1..100
$startTime = Get-Date

for ($i = 0; $i -lt $items.Count; $i++) {
    # Calculate progress
    $percentComplete = (($i + 1) / $items.Count) * 100
    $elapsed = (Get-Date) - $startTime
    $estimatedTotal = $elapsed.TotalSeconds / ($i + 1) * $items.Count
    $remaining = $estimatedTotal - $elapsed.TotalSeconds

    Write-Progress -Activity "Processing items" `
                   -Status "Item $($i + 1) of $($items.Count)" `
                   -PercentComplete $percentComplete `
                   -SecondsRemaining $remaining

    # Simulate work
    Start-Sleep -Milliseconds 500
}

Write-Progress -Activity "Processing items" -Completed

Filtering & Selection Patterns

Pattern: Multi-Condition Filter

When to use: Complex filtering logic

# Get processes that meet multiple criteria
$processes = Get-Process | Where-Object {
    # High CPU OR high memory
    (($_.CPU -gt 100) -or ($_.WS -gt 500MB)) -and
    # AND not a system process
    ($_.Company -ne $null) -and
    # AND running for more than 1 hour
    ((Get-Date) - $_.StartTime).TotalHours -gt 1
}

$processes | Format-Table Name, CPU, @{Name="MemoryMB";Expression={[math]::Round($_.WS/1MB,2)}}

Pattern: Dynamic Property Selection

When to use: Choosing which properties to display based on conditions

$verboseMode = $true  # or $false

$properties = @("Name", "Status")

if ($verboseMode) {
    $properties += "StartType", "DisplayName", "DependentServices"
}

Get-Service | Select-Object -Property $properties | Format-Table

Parallel Processing Patterns

Pattern: Process Items in Parallel (PowerShell 7+)

When to use: Speed up processing of independent items

# Sequential (slow)
$files = Get-ChildItem -Path "C:\Data" -File
$files | ForEach-Object {
    $hash = Get-FileHash -Path $_.FullName
    [PSCustomObject]@{
        File = $_.Name
        Hash = $hash.Hash
    }
}

# Parallel (fast)
$results = $files | ForEach-Object -Parallel {
    $hash = Get-FileHash -Path $_.FullName
    [PSCustomObject]@{
        File = $_.Name
        Hash = $hash.Hash
    }
} -ThrottleLimit 5  # Max 5 at a time

$results | Format-Table

Testing & Validation Patterns

Pattern: Dry Run / WhatIf Mode

When to use: Testing scripts before running them for real

param([switch]$WhatIf)

$files = Get-ChildItem -Path "C:\Temp" -File

foreach ($file in $files) {
    if ($WhatIf) {
        Write-Output "[WHATIF] Would delete: $($file.FullName)"
    } else {
        Remove-Item -Path $file.FullName -Force
        Write-Output "Deleted: $($file.FullName)"
    }
}

# Usage:
# .\script.ps1 -WhatIf    # Preview only
# .\script.ps1            # Actually run

Common Mistakes

Not Using -ErrorAction

# BAD - Errors stop execution unexpectedly
Get-ChildItem -Path "C:\Restricted"

# GOOD - Control error behavior
Get-ChildItem -Path "C:\Restricted" -ErrorAction SilentlyContinue

Hardcoding Paths

# BAD - Only works on one computer
$logPath = "C:\Users\Raymond\Documents\log.txt"

# GOOD - Works anywhere
$logPath = Join-Path $env:USERPROFILE "Documents\log.txt"

Not Validating Input

# BAD - Assumes input is valid
Remove-Item -Path $userInput -Recurse -Force

# GOOD - Validate first
if (Test-Path $userInput) {
Remove-Item -Path $userInput -Recurse -Force
} else {
Write-Error "Path not found: $userInput"
}

Additional Resources