Introduction
Getting Started
- QuickStart
Patterns
- Languages
- Supported Languages
- Python
- Java
- JavaScript
- TypeScript
- Node.js
- React
- Fastify
- Next.js
- Terraform
- C#
- C++
- C
- Go
- Rust
- Swift
- React Native
- Spring Boot
- Kotlin
- Flutter
- Ruby
- PHP
- Scala
- Perl
- R
- Dart
- Elixir
- Erlang
- Haskell
- Lua
- Julia
- Clojure
- Groovy
- Fortran
- COBOL
- Pascal
- Assembly
- Bash
- PowerShell
- SQL
- PL/SQL
- T-SQL
- MATLAB
- Objective-C
- VBA
- ABAP
- Apex
- Apache Camel
- Crystal
- D
- Delphi
- Elm
- F#
- Hack
- Lisp
- OCaml
- Prolog
- Racket
- Scheme
- Solidity
- Verilog
- VHDL
- Zig
- MongoDB
- ClickHouse
- MySQL
- GraphQL
- Redis
- Cassandra
- Elasticsearch
- Security
- Performance
Integrations
- Code Repositories
- Team Messengers
- Ticketing
Enterprise
PowerShell is a task automation and configuration management framework from Microsoft, consisting of a command-line shell and associated scripting language built on .NET. PowerShell helps system administrators and power-users rapidly automate tasks that manage operating systems and processes.
PowerShell, despite its power and flexibility, has several common anti-patterns that can lead to performance issues, security vulnerabilities, and maintainability problems. Here are the most important anti-patterns to avoid when writing PowerShell scripts.
# Anti-pattern: Using Invoke-Expression with string concatenation
$command = "Get-Process -Name " + $processName
Invoke-Expression $command
# Better approach: Use parameters and splatting
Get-Process -Name $processName
# For dynamic parameter names, use splatting
$params = @{
Name = $processName
}
Get-Process @params
Avoid using Invoke-Expression
(or its alias iex
) when possible, especially with user input or external data. It can lead to code injection vulnerabilities. Use parameters, splatting, or the call operator (&
) instead.
# Anti-pattern: No error handling
Remove-Item $path
Copy-Item $source $destination
# Better approach: Use try/catch blocks
try {
Remove-Item $path -ErrorAction Stop
Copy-Item $source $destination -ErrorAction Stop
}
catch {
Write-Error "An error occurred: $_"
# Handle the error appropriately
}
# Alternative: Use -ErrorAction and -ErrorVariable
Remove-Item $path -ErrorAction SilentlyContinue -ErrorVariable removeError
if ($removeError) {
Write-Warning "Could not remove $path: $removeError"
}
Use proper error handling with try/catch
blocks or the -ErrorAction
and -ErrorVariable
parameters. This helps you handle errors gracefully and provide meaningful feedback to users.
# Anti-pattern: Using Select-String for simple pattern matching
$text = "The quick brown fox"
if ($text | Select-String "fox") {
# Do something
}
# Better approach: Use regex methods or -match operator
$text = "The quick brown fox"
if ($text -match "fox") {
# Do something
}
# For complex regex with capture groups
$text = "ID: 12345"
if ($text -match "ID:\s*(\d+)") {
$id = $matches[1] # Access captured group
Write-Output "Found ID: $id"
}
Use the -match
operator or regex methods instead of Select-String
for simple pattern matching in strings. Select-String
is designed for searching through files or collections of strings, not for simple string operations.
# Anti-pattern: Using ForEach-Object unnecessarily
Get-Process | ForEach-Object { $_.Name }
# Better approach: Use Select-Object
Get-Process | Select-Object -ExpandProperty Name
# Or even better for this simple case
(Get-Process).Name
Avoid using ForEach-Object
(or its alias %
) when simpler alternatives exist. For property extraction, use Select-Object -ExpandProperty
or direct property access. ForEach-Object
is slower and more verbose for simple operations.
# Anti-pattern: Not using the pipeline effectively
$processes = Get-Process
$filteredProcesses = @()
foreach ($process in $processes) {
if ($process.WorkingSet -gt 100MB) {
$filteredProcesses += $process
}
}
$filteredProcesses | Sort-Object CPU -Descending | Select-Object -First 5
# Better approach: Use the pipeline effectively
Get-Process |
Where-Object { $_.WorkingSet -gt 100MB } |
Sort-Object CPU -Descending |
Select-Object -First 5
Use PowerShell’s pipeline to chain commands together. This is more idiomatic, often more readable, and can be more efficient as it processes objects one at a time through the pipeline rather than storing intermediate collections.
# Anti-pattern: Using Write-Host for output
function Get-UserInfo {
Write-Host "User: $env:USERNAME"
Write-Host "Domain: $env:USERDOMAIN"
}
# This won't work because Write-Host doesn't output to the pipeline
$userInfo = Get-UserInfo | Select-String "Domain"
# Better approach: Use Write-Output for data
function Get-UserInfo {
Write-Output "User: $env:USERNAME"
Write-Output "Domain: $env:USERDOMAIN"
}
# Now this works
$userInfo = Get-UserInfo | Select-String "Domain"
# Use Write-Host only for UI/visual output that isn't meant to be processed
function Show-Status {
Write-Host "Processing..." -ForegroundColor Green
}
Use Write-Output
(or simply let PowerShell implicitly output objects) for data that might be consumed by other commands in a pipeline. Use Write-Host
only for visual feedback that isn’t meant to be processed further.
# Anti-pattern: Manual parameter validation
function Get-FileContent {
param($Path)
if (-not $Path) {
throw "Path parameter is required"
}
if (-not (Test-Path $Path)) {
throw "File not found: $Path"
}
Get-Content $Path
}
# Better approach: Use parameter validation attributes
function Get-FileContent {
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path $_ })]
[string]$Path
)
Get-Content $Path
}
Use PowerShell’s parameter validation attributes instead of writing manual validation code. This makes your functions more robust, self-documenting, and consistent with PowerShell conventions.
# Anti-pattern: String concatenation
$message = "User " + $username + " logged in at " + $time
# Better approach: Format operator
$message = "User $username logged in at $time"
# For complex formatting
$message = [string]::Format("User {0} logged in at {1:HH:mm:ss}", $username, $time)
# Or with PowerShell's -f operator
$message = "User {0} logged in at {1:HH:mm:ss}" -f $username, $time
Use string interpolation, the format operator (-f
), or [string]::Format()
instead of string concatenation. These methods are more readable and efficient, especially for complex strings.
# Anti-pattern: Not using strong types
function Add-Numbers {
param($a, $b)
return $a + $b
}
# This might not work as expected
Add-Numbers "5" "10" # Returns "510" (string concatenation)
# Better approach: Use strong types
function Add-Numbers {
param(
[int]$a,
[int]$b
)
return $a + $b
}
# Now this works correctly
Add-Numbers "5" "10" # Returns 15 (numeric addition)
Use strong types for function parameters and variables when appropriate. This helps catch type-related errors early and makes your code more self-documenting and robust.
# Anti-pattern: Using aliases in scripts
gps | ? { $_.WS -gt 100MB } | sort CPU -desc | select -first 5
# Better approach: Use full command and parameter names
Get-Process |
Where-Object { $_.WorkingSet -gt 100MB } |
Sort-Object -Property CPU -Descending |
Select-Object -First 5
Avoid using aliases (like gps
, ?
, %
, sort
, select
) in scripts and functions. Use full command and parameter names for better readability, maintainability, and to avoid issues if aliases change or aren’t available in all environments.
# Anti-pattern: Using hashtables or arrays for structured data
$user = @{
Name = "John Doe"
Email = "john@example.com"
Age = 30
}
Write-Output $user.Name
# Better approach: Use PSCustomObject
$user = [PSCustomObject]@{
Name = "John Doe"
Email = "john@example.com"
Age = 30
}
Write-Output $user.Name
# PSCustomObject works better with the pipeline
$users = @(
[PSCustomObject]@{ Name = "John"; Age = 30 },
[PSCustomObject]@{ Name = "Jane"; Age = 28 }
)
$users | Where-Object { $_.Age -lt 30 }
Use [PSCustomObject]
for structured data instead of hashtables or arrays. It provides better property access syntax, works well with the pipeline, and is more idiomatic in PowerShell.
# Anti-pattern: Using global variables unnecessarily
$config = @{ Server = "localhost"; Port = 8080 }
function Connect-Server {
# Uses the global $config variable
$connection = New-Object System.Net.Sockets.TcpClient $config.Server, $config.Port
return $connection
}
# Better approach: Pass parameters explicitly
function Connect-Server {
param(
[string]$Server = "localhost",
[int]$Port = 8080
)
$connection = New-Object System.Net.Sockets.TcpClient $Server, $Port
return $connection
}
# Or use script scope when appropriate
$script:config = @{ Server = "localhost"; Port = 8080 }
Be mindful of variable scopes. Avoid using global variables unnecessarily. Pass parameters explicitly to functions or use appropriate scopes ($script:
, $local:
, $private:
) to limit variable visibility.
# Anti-pattern: Not using Begin/Process/End blocks
function Process-Items {
param(
[Parameter(ValueFromPipeline = $true)]
[string[]]$Items
)
foreach ($item in $Items) {
"Processing $item"
}
}
# This won't work as expected with pipeline input
"item1", "item2" | Process-Items
# Better approach: Use Begin/Process/End blocks
function Process-Items {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline = $true)]
[string]$Item
)
begin {
"Starting processing"
$count = 0
}
process {
"Processing $Item"
$count++
}
end {
"Processed $count items"
}
}
# Now this works correctly with pipeline input
"item1", "item2" | Process-Items
Use begin
, process
, and end
blocks in advanced functions that accept pipeline input. The process
block runs once for each pipeline object, while begin
and end
run once before and after processing.
# Anti-pattern: Simple function without CmdletBinding
function Get-UserData {
param($Username)
# Function logic...
}
# Better approach: Use CmdletBinding
function Get-UserData {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Username
)
# Function logic...
}
Use [CmdletBinding()]
for your functions to make them behave more like cmdlets. This gives you access to common parameters like -Verbose
, -Debug
, and -ErrorAction
, and enables better error handling and parameter validation.
# Anti-pattern: Using Write-Debug or Write-Verbose without CmdletBinding
function Process-Data {
param($Data)
Write-Verbose "Processing data..."
Write-Debug "Data value: $Data"
# Process data...
}
# Better approach: Use CmdletBinding
function Process-Data {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
$Data
)
Write-Verbose "Processing data..."
Write-Debug "Data value: $Data"
# Process data...
}
Always use [CmdletBinding()]
when using Write-Verbose
or Write-Debug
. This ensures that these messages are only displayed when the corresponding preference variables are set or when the -Verbose
or -Debug
switches are used.
# Anti-pattern: Not specifying output type
function Get-User {
param([string]$Username)
# Return user object
return @{
Username = $Username
FullName = "John Doe"
Email = "$Username@example.com"
}
}
# Better approach: Specify output type
function Get-User {
[CmdletBinding()]
[OutputType([PSCustomObject])]
param([string]$Username)
# Return user object
return [PSCustomObject]@{
Username = $Username
FullName = "John Doe"
Email = "$Username@example.com"
}
}
Use [OutputType()]
to specify the type of objects your function returns. This helps with documentation, IntelliSense, and makes your code more self-documenting.
# Anti-pattern: All functions in one file without exports
# MyModule.psm1
function Get-Something { /* ... */ }
function Set-Something { /* ... */ }
function Internal-Helper { /* ... */ }
# Better approach: Proper module structure with exports
# MyModule.psm1
function Get-Something { /* ... */ }
function Set-Something { /* ... */ }
function Internal-Helper { /* ... */ }
# Only export the public functions
Export-ModuleMember -Function Get-Something, Set-Something
# Even better: Use a module manifest
# MyModule.psd1
@{
ModuleVersion = '1.0'
GUID = 'unique-guid-here'
Author = 'Your Name'
FunctionsToExport = @('Get-Something', 'Set-Something')
# Other metadata...
}
Use proper module structure with explicit exports and module manifests. This helps control which functions are exposed to users and provides important metadata about your module.
# Anti-pattern: No or minimal documentation
function Get-UserData {
param($Username)
# Function logic...
}
# Better approach: Use comment-based help
<#
.SYNOPSIS
Retrieves user data from the system.
.DESCRIPTION
The Get-UserData function retrieves detailed information about a user
from the system database.
.PARAMETER Username
The username of the user to retrieve data for.
.EXAMPLE
Get-UserData -Username "john.doe"
Retrieves data for user john.doe.
.NOTES
Author: Your Name
Date: Current Date
#>
function Get-UserData {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Username
)
# Function logic...
}
Use comment-based help to document your functions and scripts. This provides help information to users via the Get-Help
cmdlet and makes your code more maintainable.
# Anti-pattern: Using Invoke-WebRequest in a loop
$urls = @("https://example.com/1", "https://example.com/2", "https://example.com/3")
$results = @()
foreach ($url in $urls) {
$response = Invoke-WebRequest -Uri $url
$results += $response
}
# Better approach: Use parallelism
$urls = @("https://example.com/1", "https://example.com/2", "https://example.com/3")
$jobs = $urls | ForEach-Object {
$url = $_
Start-Job -ScriptBlock {
param($u)
Invoke-WebRequest -Uri $u
} -ArgumentList $url
}
$results = $jobs | Wait-Job | Receive-Job
Remove-Job $jobs
# Even better: Use Invoke-Parallel or similar
$results = $urls | Invoke-Parallel -ScriptBlock {
Invoke-WebRequest -Uri $_
}
Avoid using Invoke-WebRequest
sequentially in loops for multiple requests. Use parallelism with Start-Job
, ForEach-Object -Parallel
(in PowerShell 7+), or a module like PSParallel
for better performance.
# Anti-pattern: Using .NET methods directly for simple file operations
[System.IO.File]::ReadAllText("C:\path\to\file.txt")
[System.IO.File]::WriteAllText("C:\path\to\output.txt", $content)
# Better approach: Use PowerShell cmdlets
Get-Content -Path "C:\path\to\file.txt" -Raw
Set-Content -Path "C:\path\to\output.txt" -Value $content
Use PowerShell cmdlets like Get-Content
, Set-Content
, Get-ChildItem
, etc., for file operations instead of .NET methods directly. The cmdlets are more idiomatic in PowerShell and often provide additional functionality like handling different encodings, working with the pipeline, and supporting PowerShell paths.
# Anti-pattern: Using == and != operators
if ($value == 5) { # This works in PowerShell 7+ but not in Windows PowerShell
# Do something
}
if ($name != "John") {
# Do something
}
# Better approach: Use -eq and -ne operators
if ($value -eq 5) {
# Do something
}
if ($name -ne "John") {
# Do something
}
Use PowerShell’s comparison operators (-eq
, -ne
, -gt
, -lt
, etc.) instead of C-style operators (==
, !=
, >
, <
, etc.). The PowerShell operators work consistently across all PowerShell versions and have special behavior for collections and wildcards.
# Anti-pattern: Not specifying case sensitivity
if ($name -eq "john") {
# This might not match "John" as expected
}
# Better approach: Specify case sensitivity explicitly
if ($name -eq "john" -or $name -eq "John") {
# Explicit but verbose
}
# Even better: Use case-insensitive comparison
if ($name -ieq "john") {
# Case-insensitive comparison
}
Be explicit about case sensitivity in string comparisons. Use -ieq
, -ine
, etc., for case-insensitive comparisons, and -ceq
, -cne
, etc., for case-sensitive comparisons.