Control Flow
Summary: in this tutorial, you will learn master conditional statements (if/elseif/else, switch) and counted iteration (for, foreach) in powershell.
Control Flow
Control flow is how your scripts make decisions and repeat actions. Without control flow, code would execute line-by-line from top to bottom with no ability to branch based on conditions or repeat operations. Control flow statements are the fundamental building blocks that transform simple command sequences into intelligent, adaptive programs.
PowerShell provides all the standard control flow constructs you'd expect from a modern programming language — if, switch, for, foreach, while — plus some unique features that make scripts more concise and expressive.
Understanding Conditional Logic
Before diving into syntax, let's understand what makes conditional logic powerful. Your scripts need to adapt to different situations:
- Is the file present before trying to read it?
- Is the user authorized before granting access?
- Did the network request succeed before processing the response?
- Is this value within valid range before performing calculations?
Conditions let your code branch: "If this is true, do this; otherwise, do that." This adaptability is what separates simple automation from intelligent programs.
If / ElseIf / Else: Basic Decision Making
The if statement is the most fundamental conditional construct. It evaluates a condition and executes a block of code only if that condition is true.
Simple If Statements
$temperature = 72
if ($temperature -gt 85) {
Write-Host "It's hot outside! Stay hydrated."
}
If $temperature is greater than 85, the message displays. Otherwise, nothing happens and execution continues to the next statement.
If-Else: Binary Decisions
When you have two mutually exclusive branches, use else:
$temperature = 72
if ($temperature -gt 85) {
Write-Host "It's hot! Turn on the AC."
} else {
Write-Host "Temperature is comfortable."
}
Exactly one of these blocks will execute — never both, never neither.
If-ElseIf-Else: Multiple Conditions
For more than two outcomes, chain conditions with elseif:
$temperature = 72
if ($temperature -gt 85) {
"It's hot!"
} elseif ($temperature -gt 65) {
"It's nice out."
} elseif ($temperature -gt 45) {
"It's cool."
} else {
"It's cold!"
}
PowerShell evaluates these conditions sequentially from top to bottom and executes the first matching block. Once a match is found, the remaining conditions are skipped. This ordering is crucial — if you put broad conditions before specific ones, the specific conditions will never execute.
Complex Conditions with Logical Operators
Real-world conditions often involve multiple factors:
$hour = (Get-Date).Hour
$day = (Get-Date).DayOfWeek
if ($day -in 'Saturday','Sunday') {
"Weekend - enjoy your day off!"
} elseif ($hour -lt 9 -or $hour -ge 17) {
"Outside of work hours"
} elseif ($hour -ge 12 -and $hour -lt 13) {
"Lunch time!"
} else {
"Working hours - stay productive!"
}
The -and operator requires both conditions to be true. The -or operator requires at least one to be true. You can combine these with parentheses for complex logic:
if (($status -eq "Active" -and $balance -gt 0) -or $isPremium) {
"Account has full access"
}
If Statements as Expressions
Unlike many languages where if is purely a statement, PowerShell's if can return values:
$status = if ($count -gt 0) { "Active" } else { "Empty" }
The last expression evaluated in the chosen block becomes the return value. This makes assignments based on conditions more concise than storing results in separate statements.
In PowerShell 7+, you can use the even more concise ternary operator:
$status = $count -gt 0 ? "Active" : "Empty"
Both forms produce the same result — choose whichever is clearer in context.
Handling Null and Empty Values
PowerShell provides several ways to check for null or empty values, each with different semantics:
$value = $null
# Check for null explicitly
if ($null -eq $value) {
"Value is null"
}
# Check for empty string
if ([string]::IsNullOrEmpty($value)) {
"Value is null or empty string"
}
# Check for null, empty, or whitespace
if ([string]::IsNullOrWhiteSpace($value)) {
"Value is null, empty, or only whitespace"
}
# Check for empty collection
if ($collection.Count -eq 0) {
"Collection has no items"
}
Critical: Always put $null on the left side of comparisons
if ($null -eq $value) { ... } # ✅ Correct
if ($value -eq $null) { ... } # ❌ Can give wrong results
Why? When $value is an array, putting it on the left causes PowerShell to use -eq as a filter operator, returning matching elements rather than a boolean. With $null on the left, PowerShell correctly performs a boolean equality test.
$arr = @(1, 2, $null, 4)
$arr -eq $null # Returns @($null) — the filtered array
$null -eq $arr # Returns $false — correct boolean result
Switch Statement: PowerShell's Most Powerful Conditional
PowerShell's switch statement is far more capable than the switch found in languages like C# or Java. It supports pattern matching, regular expressions, evaluating multiple matches, and even processing entire files.
Basic Switch Syntax
$fruit = "apple"
switch ($fruit) {
"apple" { "It's red or green, crispy and sweet" }
"banana" { "It's yellow and soft" }
"cherry" { "It's small and red" }
default { "Unknown fruit: $fruit" }
}
The switch evaluates the input value against each case. When a match is found, it executes that block. The default case acts as a catch-all for values that don't match any explicit case.
Multiple Matches: Switch Evaluates All Cases
Unlike C#/Java where switch stops after the first match, PowerShell's switch evaluates ALL matching cases by default:
$value = 3
switch ($value) {
{$_ -gt 0} { "Positive" }
{$_ -lt 10} { "Single digit" }
{$_ % 2 -eq 1} { "Odd" }
}
# Output:
# Positive
# Single digit
# Odd
All three conditions match, so all three blocks execute. This behavior is often useful for categorizing data into multiple non-exclusive categories.
If you want to stop after the first match, use break:
$value = 3
switch ($value) {
{$_ -gt 0} { "Positive"; break }
{$_ -lt 10} { "Single digit"; break }
}
# Output: Positive (stops after first match)
Within switch blocks, the special variable $_ refers to the current value being tested.
Pattern Matching with Wildcards
Use the -Wildcard parameter to match patterns:
$filename = "report-2025.pdf"
switch -Wildcard ($filename) {
"*.pdf" { "PDF document" }
"*.docx" { "Word document" }
"*.xlsx" { "Excel spreadsheet" }
"report*" { "This is a report file" }
}
# Output:
# PDF document
# This is a report file
Both patterns match, so both blocks execute. Wildcards use * (zero or more characters), ? (exactly one character), and [abc] (character class) patterns.
Pattern Matching with Regular Expressions
For complex patterns, use the -Regex parameter:
$email = "admin@example.com"
switch -Regex ($email) {
"^admin" { "Admin account detected" }
"@example\.com$" { "Internal company email" }
"^\w+@\w+\.\w+$" { "Valid basic email format" }
}
# All three patterns match
Regular expressions provide much more power than wildcards, enabling validation of complex formats like emails, phone numbers, IP addresses, and structured data.
When using -Regex, you can access captured groups through the $Matches automatic variable:
$version = "PowerShell 7.4.1"
switch -Regex ($version) {
"(\d+)\.(\d+)\.(\d+)" {
$major = $Matches[1]
$minor = $Matches[2]
$patch = $Matches[3]
"Version breakdown: Major=$major, Minor=$minor, Patch=$patch"
}
}
Case-Sensitive Matching
By default, switch is case-insensitive. Use -CaseSensitive for exact matching:
$command = "EXIT"
switch -CaseSensitive ($command) {
"exit" { "Lowercase exit command" }
"EXIT" { "Uppercase EXIT command" }
"Exit" { "Title case Exit command" }
}
# Output: Uppercase EXIT command
Switch with Arrays: Automatic Iteration
One of PowerShell's most useful switch features: when you provide an array as input, switch automatically iterates over each element:
$colors = "red", "blue", "green", "yellow"
switch ($colors) {
"red" { "$_ is a warm color" }
"blue" { "$_ is a cool color" }
"green" { "$_ is a cool color" }
default { "$_ is not classified" }
}
# Output:
# red is a warm color
# blue is a cool color
# green is a cool color
# yellow is not classified
This processes each array element through the switch logic, which is incredibly concise compared to wrapping a switch in a foreach loop.
Switch with File Input: Processing Line by Line
The -File parameter makes switch read a file and process each line:
switch -Regex -File "C:\Logs\application.log" {
"ERROR" { Write-Host "Error found: $_" -ForegroundColor Red }
"WARNING" { Write-Host "Warning found: $_" -ForegroundColor Yellow }
"^\d{4}-\d{2}-\d{2}" { Write-Host "Dated entry: $_" }
}
This is remarkably efficient for log file processing — PowerShell reads the file line-by-line (streaming, not loading entire file into memory) and evaluates each line against your patterns. This is perfect for analyzing logs, configuration files, or any line-based text data.
Practical Switch Example: Seasonal Greetings
function Get-SeasonalGreeting {
param([int]$Month = (Get-Date).Month)
switch ($Month) {
{$_ -in 3,4,5} { "Spring has sprung! 🌸" }
{$_ -in 6,7,8} { "Summer vibes! ☀️" }
{$_ -in 9,10,11} { "Fall colors! 🍂" }
{$_ -in 12,1,2} { "Winter wonderland! ❄️" }
}
}
Get-SeasonalGreeting # Uses current month
Get-SeasonalGreeting -Month 7 # "Summer vibes! ☀️"
By using script blocks with conditions, you can group related values and express complex logic clearly.
For Loop: Counted Iteration
The for loop is your tool for counted iteration — when you know exactly how many times you want to repeat an operation or when you need fine control over loop variables.
Basic For Loop Structure
for ($i = 0; $i -lt 5; $i++) {
"Iteration $i"
}
# Output:
# Iteration 0
# Iteration 1
# Iteration 2
# Iteration 3
# Iteration 4
The for loop has three parts (separated by semicolons):
- Initialization:
$i = 0— runs once before loop starts - Condition:
$i -lt 5— checked before each iteration; loop continues while true - Increment:
$i++— runs after each iteration
Counting Down
for ($i = 10; $i -gt 0; $i--) {
"$i..."
Start-Sleep -Milliseconds 500
}
Write-Host "Launch! 🚀" -ForegroundColor Green
By changing the condition and increment, you can count in any direction.
Custom Step Sizes
for ($i = 0; $i -le 100; $i += 10) {
"Progress: $i%"
}
# Output: Progress: 0%, Progress: 10%, ..., Progress: 100%
Use += with any value to control the increment size.
Multiple Loop Variables
You can initialize and update multiple variables:
for ($i = 0, $j = 10; $i -lt $j; $i++, $j--) {
"i=$i, j=$j, sum=$($i+$j)"
}
# Output:
# i=0, j=10, sum=10
# i=1, j=9, sum=10
# i=2, j=8, sum=10
# i=3, j=7, sum=10
# i=4, j=6, sum=10
This pattern is useful for algorithms that need converging or diverging indices.
When to Use For vs Other Loops
Use for when:
- You know the exact number of iterations
- You need an index variable
- You're implementing mathematical algorithms
- You need precise control over initialization and increment logic
Use foreach (next section) when you're iterating over collections and don't need index management.
Foreach Loop: Iterating Over Collections
The foreach loop iterates over each item in a collection. It's the most natural way to process arrays, lists, files, and pipeline output.
Basic Foreach Syntax
$fruits = @("apple", "banana", "cherry")
foreach ($fruit in $fruits) {
"I like $fruit"
}
# Output:
# I like apple
# I like banana
# I like cherry
The loop variable ($fruit) takes on each value from the collection in sequence.
Iterating Over Files
foreach ($file in Get-ChildItem *.txt) {
"File: $($file.Name)"
"Size: $($file.Length) bytes"
"Modified: $($file.LastWriteTime)"
""
}
This pattern processes each file returned by Get-ChildItem, accessing properties of the file object.
Iterating Over Hashtables
Hashtables require special handling because they have keys and values:
$config = @{
Host = "localhost"
Port = 8080
Debug = $true
LogLevel = "Info"
}
# Method 1: Iterate over keys
foreach ($key in $config.Keys) {
"$key = $($config[$key])"
}
# Method 2: Use GetEnumerator (gives you key-value pairs)
foreach ($entry in $config.GetEnumerator()) {
"$($entry.Key) = $($entry.Value)"
}
GetEnumerator() returns objects with .Key and .Value properties, which is often more convenient than accessing keys and looking up values separately.
Modifying Collections During Iteration
Do not modify a collection while iterating over it with foreach
$numbers = @(1,2,3,4,5)
foreach ($num in $numbers) {
if ($num -gt 3) {
$numbers += 10 # ❌ Modifying collection being iterated
}
}
This can cause unpredictable behavior. Instead, build a new collection or use a for loop with explicit indices.
Foreach Statement vs ForEach-Object Cmdlet
PowerShell has two constructs that look similar but behave differently:
foreach statement:
foreach ($proc in Get-Process) {
$proc.Name
}
ForEach-Object cmdlet:
Get-Process | ForEach-Object {
$_.Name
}
Key differences:
foreach statement:
- Loads the entire collection into memory first
- Faster for small to medium datasets
- Can use
breakto exit early - Cannot be used in pipeline chains
ForEach-Object cmdlet:
- Processes items one at a time from the pipeline (streaming)
- Better for large datasets or infinite streams
- Essential for pipeline processing
- Can be interrupted with Ctrl+C more responsively
Choose foreach for in-memory collections where you need loop control. Choose ForEach-Object when working with pipelines or very large datasets.
Written by the ShellRAG Team
The ShellRAG editorial team writes practical, beginner-friendly PowerShell tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.
Learn more about us →