Skip to content

PowerShell Scripting Best Practices Guide

This article provides a comprehensive guide on PowerShell scripting best practices, focusing on code structure, output formatting, error handling, performance optimization, and security measures.

Following best practices in PowerShell scripting ensures your scripts are readable, maintainable, secure, and performant. It reduces technical debt, enhances collaboration, and minimizes risks in production environments.

Decide Whether You’re Coding a ‘Tool’ or a ‘Controller’

Section titled “Decide Whether You’re Coding a ‘Tool’ or a ‘Controller’”
  • Tool: Reusable functions/modules.
  • Controller: Automates a specific task, not designed for reuse.
  • Use functions and script modules to maximize reusability.
  • Follow Verb-Noun format using approved PowerShell verbs (Get-Verb).
  • Use names like $ComputerName instead of custom prefixes.
  • Tools should output minimally processed data for flexibility.
  • Controllers can format data for user-friendly reports.
Terminal window
function Get-DiskInfo {
param ([string]$ComputerName)
Get-WmiObject Win32_LogicalDisk -ComputerName $ComputerName
}

Use built-in cmdlets like Test-Connection instead of custom ping functions.

Terminal window
# Preferred
Test-Connection $ComputerName -Quiet

Include comment-based help with .SYNOPSIS, .DESCRIPTION, and at least one .EXAMPLE.

Terminal window
function Test-Help {
<#
.SYNOPSIS
Demonstrates proper help documentation.
.EXAMPLE
Test-Help -MandatoryParameter "Example"
Runs the Test-Help function with a mandatory parameter.
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Alias("MP")]
[String]$MandatoryParameter
)
}

Enables common parameters like -Verbose, -Debug, -ErrorAction.

For state-changing commands, use SupportsShouldProcess.

Terminal window
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = "Medium")]
param ([switch]$Force)

Always define parameter types for validation and clarity.

Terminal window
param (
[string]$Name,
[int]$Count
)
  • Defaults to $false.
  • Use boolean logic, avoid treating it as three-state.

Use Write-Verbose, Write-Debug, or Write-Output appropriately.

Terminal window
Write-Progress -Activity "Processing" -Status "50% Complete" -PercentComplete 50

Define .format.ps1xml files instead of inline formatting.

Use [OutputType()] and avoid mixing object types.


Force terminating errors to handle them with try-catch.

Terminal window
try {
Get-Item "C:\InvalidPath" -ErrorAction Stop
} catch {
Write-Warning "Item not found."
}

Use $ErrorActionPreference for Non-Cmdlets

Section titled “Use $ErrorActionPreference for Non-Cmdlets”

Temporarily set to 'Stop' around risky operations.

Use structured try-catch blocks instead.

Terminal window
catch {
$errorDetails = $_
Write-Error "An error occurred: $($errorDetails.Exception.Message)"
}

PERF-01 Measure Performance When It Matters

Section titled “PERF-01 Measure Performance When It Matters”

Use Measure-Command to benchmark different approaches, especially with large datasets.

Terminal window
Measure-Command {
foreach ($item in $data) { Process-Item $item }
}

PERF-02 Balance Performance and Readability

Section titled “PERF-02 Balance Performance and Readability”
  • For small datasets, prioritize readability.
  • For large datasets, consider streaming and low-level .NET techniques if necessary.

Readable but less performant:

Terminal window
$content = Get-Content -Path file.txt
foreach ($line in $content) {
Do-Something -Input $line
}

Streamlined for performance:

Terminal window
Get-Content -Path file.txt | ForEach-Object {
Do-Something -Input $_
}

High-performance with .NET:

Terminal window
$sr = New-Object System.IO.StreamReader "file.txt"
while ($sr.Peek() -ge 0) {
$line = $sr.ReadLine()
Do-Something -Input $line
}

PERF-03 Prefer Language Features Over Cmdlets for Speed

Section titled “PERF-03 Prefer Language Features Over Cmdlets for Speed”
  • Language constructs (foreach) > .NET methods > Scripts > Cmdlets/Pipeline
  • Always measure before optimizing prematurely.

Avoid plain text passwords. Accept credentials as parameters using [Credential()].

Terminal window
param (
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$Credential
)

If passing to APIs:

Terminal window
$Insecure.SetPassword($Credential.GetNetworkCredential().Password)

Prompt securely and store encrypted values.

Terminal window
$Secure = Read-Host -Prompt "Enter Secure Data" -AsSecureString

Convert SecureString to plain text safely:

Terminal window
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure)
$PlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)

Use Export-CliXml for storing credentials.

Terminal window
Get-Credential | Export-CliXml -Path C:\secure\cred.xml
$Credential = Import-CliXml -Path C:\secure\cred.xml
Terminal window
ConvertFrom-SecureString -SecureString $Secure | Out-File -Path "${Env:AppData}\secure.bin"
$Secure = Get-Content -Path "${Env:AppData}\secure.bin" | ConvertTo-SecureString

By following these PowerShell best practices across design, documentation, output handling, error management, performance, and security, you can create robust, maintainable, and efficient scripts suitable for both small tasks and enterprise-level automation. Always strive to balance readability, performance, and security to deliver high-quality solutions.