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
Hardcoding Paths
Not Validating Input
Related Topics
- Your First Script - Building complete scripts
- Functions - Creating reusable code blocks
- Error Handling - Robust error management
- Script Structure - Organizing larger scripts
Additional Resources
- PowerShell Best Practices
- PowerShell Style Guide
- PowerShell Gallery - Real-world examples