Loops and Iteration Patterns
Summary: in this tutorial, you will learn master powershell loops: while, do-while, do-until, break, continue, and practical control flow patterns for real-world automation.
Loops and Iteration Patterns
With conditional logic and basic for/foreach loops covered, this tutorial dives into the remaining loop constructs and the patterns that tie everything together. You'll learn while loops for condition-based iteration, do-while/do-until for guaranteed execution, flow control with break and continue, and practical patterns used in production scripts.
While Loop: Condition-Based Iteration
The while loop repeats as long as a condition remains true. It's useful when you don't know in advance how many iterations will be needed.
Basic While Loop
$count = 0
while ($count -lt 5) {
"Count: $count"
$count++
}
The condition is checked before each iteration. If it's initially false, the loop body never executes.
Waiting for a Condition
A common pattern is polling for something to become true:
$timeout = 30
$elapsed = 0
while (-not (Test-Path "ready.txt") -and $elapsed -lt $timeout) {
Write-Host "Waiting for ready.txt... ($elapsed seconds elapsed)"
Start-Sleep -Seconds 1
$elapsed++
}
if (Test-Path "ready.txt") {
"File found! Proceeding..."
} else {
"Timeout — file did not appear within $timeout seconds"
}
This loop polls every second, checking if a file exists, with a timeout to prevent infinite waiting.
Infinite Loops with Break
Sometimes you want a loop that runs indefinitely until explicitly broken:
$attempts = 0
while ($true) {
$response = Read-Host "Enter password"
$attempts++
if ($response -eq $correctPassword) {
"Access granted!"
break
}
if ($attempts -ge 3) {
"Too many failed attempts"
break
}
"Incorrect. Try again."
}
while ($true) creates an infinite loop, but break statements provide controlled exit points.
Do-While and Do-Until: Guaranteed Execution
The do-while and do-until loops differ from while in one crucial way: they check the condition after executing the loop body, guaranteeing at least one execution.
Do-While: Keep Going While True
$number = 0
do {
$number = Read-Host "Enter a number (1-10)"
} while ($number -lt 1 -or $number -gt 10)
"You entered: $number"
This prompts at least once, then keeps prompting until the user enters a valid number (1-10). The condition while ($number -lt 1 -or $number -gt 10) means "keep repeating while the number is invalid."
Do-Until: Keep Going Until True
$guess = 0
$target = Get-Random -Minimum 1 -Maximum 101
$attempts = 0
do {
$guess = [int](Read-Host "Guess a number (1-100)")
$attempts++
if ($guess -lt $target) {
Write-Host "Too low!" -ForegroundColor Yellow
} elseif ($guess -gt $target) {
Write-Host "Too high!" -ForegroundColor Yellow
}
} until ($guess -eq $target)
"Correct! You guessed it in $attempts attempts!"
The until condition means "keep repeating until the guess equals the target." This is the opposite logic from while.
When to choose which:
- Use
do-whilewhen the condition expresses "keep going while this is true" - Use
do-untilwhen the condition expresses "keep going until this becomes true"
Choose whichever makes your intent clearer.
Break and Continue: Controlling Loop Flow
break and continue give you fine-grained control over loop execution, letting you exit early or skip iterations.
Break: Exit the Loop
break immediately exits the innermost loop:
foreach ($num in 1..100) {
if ($num * $num -gt 50) {
"First number whose square exceeds 50: $num"
"($num squared is $($num*$num))"
break
}
}
# Output:
# First number whose square exceeds 50: 8
# (8 squared is 64)
Without break, this would process all 100 numbers. With break, it stops as soon as the condition is met.
Break with Labels: Exiting Nested Loops
When you have nested loops, break only exits the innermost loop by default. To break out of an outer loop, use labels:
:outer foreach ($i in 1..5) {
foreach ($j in 1..5) {
if ($i * $j -gt 10) {
"Breaking at i=$i, j=$j (product=$($i*$j))"
break outer # Breaks out of the OUTER loop
}
"$i x $j = $($i*$j)"
}
}
The label :outer names the loop, and break outer exits that specific loop rather than just the inner foreach.
Continue: Skip to Next Iteration
continue skips the rest of the current iteration and immediately starts the next one:
foreach ($num in 1..10) {
if ($num % 2 -eq 0) {
continue # Skip even numbers
}
"Odd number: $num"
}
# Output: 1, 3, 5, 7, 9
continue is perfect for filtering within loops — processing only items that meet certain criteria while skipping others.
Practical Example: Processing Files
foreach ($item in Get-ChildItem) {
# Skip directories
if ($item.PSIsContainer) {
continue
}
# Skip hidden files
if ($item.Attributes -band [System.IO.FileAttributes]::Hidden) {
continue
}
# Process only visible files
Write-Host "Processing file: $($item.Name)" -ForegroundColor Green
# ... do work with $item ...
}
Multiple continue statements filter out unwanted items before reaching the processing logic.
Practical Control Flow Patterns
Retry Logic with Exponential Backoff
Network operations often fail temporarily. Retry logic attempts the operation multiple times before giving up:
$maxRetries = 5
$retryDelay = 1 # seconds
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
try {
$result = Invoke-WebRequest "https://api.example.com/data" -TimeoutSec 10
Write-Host "Success on attempt $attempt" -ForegroundColor Green
break # Success — exit loop
} catch {
Write-Warning "Attempt $attempt failed: $($_.Exception.Message)"
if ($attempt -lt $maxRetries) {
Write-Host "Retrying in $retryDelay seconds..."
Start-Sleep -Seconds $retryDelay
$retryDelay *= 2 # Exponential backoff: 1, 2, 4, 8, 16 seconds
} else {
throw "Failed after $maxRetries attempts — giving up"
}
}
}
This pattern is essential for resilient scripts that interact with unreliable external services.
Interactive Menu System
function Show-AdminMenu {
do {
Clear-Host
Write-Host ""
Write-Host "=== System Administration Menu ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "1. Show top 10 processes by CPU"
Write-Host "2. Show disk space"
Write-Host "3. Show network configuration"
Write-Host "4. Show system uptime"
Write-Host "5. Show last 10 system events"
Write-Host "Q. Quit"
Write-Host ""
$choice = Read-Host "Select an option"
switch ($choice) {
'1' {
Get-Process |
Sort-Object CPU -Descending |
Select-Object -First 10 |
Format-Table Name, CPU, WorkingSet64, Id -AutoSize
Read-Host "Press Enter to continue"
}
'2' {
Get-PSDrive -PSProvider FileSystem |
Format-Table Name,
@{N='Used(GB)';E={[math]::Round($_.Used/1GB,2)}},
@{N='Free(GB)';E={[math]::Round($_.Free/1GB,2)}} -AutoSize
Read-Host "Press Enter to continue"
}
'3' {
Get-NetIPAddress -AddressFamily IPv4 |
Format-Table InterfaceAlias, IPAddress, PrefixLength -AutoSize
Read-Host "Press Enter to continue"
}
'4' {
$uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
"System uptime: $($uptime.Days) days, $($uptime.Hours) hours, $($uptime.Minutes) minutes"
Read-Host "Press Enter to continue"
}
'5' {
Get-EventLog -LogName System -Newest 10 |
Format-Table TimeGenerated, EntryType, Source, Message -AutoSize
Read-Host "Press Enter to continue"
}
'Q' {
Write-Host "Goodbye!" -ForegroundColor Green
return # Exit function
}
default {
Write-Host "Invalid choice — please select 1-5 or Q" -ForegroundColor Red
Start-Sleep -Seconds 2
}
}
} while ($true)
}
This pattern creates professional interactive menus that loop until the user chooses to quit.
Processing with Progress Bar
When processing large datasets, showing progress improves user experience:
$files = Get-ChildItem -Path $HOME -File -Recurse -ErrorAction SilentlyContinue
$total = $files.Count
$current = 0
$processed = 0
$errors = 0
foreach ($file in $files) {
$current++
$percent = [math]::Round(($current / $total) * 100, 1)
Write-Progress -Activity "Processing files" `
-Status "$current of $total ($percent%) - Current: $($file.Name)" `
-PercentComplete $percent `
-CurrentOperation $file.FullName
try {
# Simulate processing
if ($file.Length -gt 1MB) {
Write-Verbose "Large file: $($file.Name)"
}
$processed++
} catch {
$errors++
Write-Warning "Error processing $($file.Name): $($_.Exception.Message)"
}
}
Write-Progress -Activity "Processing files" -Completed
Write-Host ""
Write-Host "Processing complete:" -ForegroundColor Green
Write-Host " Total files: $total"
Write-Host " Processed: $processed"
Write-Host " Errors: $errors"
Write-Progress displays a progress bar that updates as the loop runs, giving users feedback on long-running operations.
Batch Processing with Error Recovery
Process items in batches, recovering from individual failures without stopping the entire operation:
$servers = @("server01", "server02", "server03", "server04", "server05")
$results = @()
foreach ($server in $servers) {
Write-Host "Processing $server..." -ForegroundColor Cyan
try {
# Test connectivity first
if (-not (Test-Connection $server -Count 1 -Quiet)) {
throw "Server unreachable"
}
# Get information
$info = Get-CimInstance Win32_OperatingSystem -ComputerName $server -ErrorAction Stop
$results += [PSCustomObject]@{
Server = $server
Status = "Success"
OS = $info.Caption
LastBoot = $info.LastBootUpTime
Error = $null
}
Write-Host " ✓ Success" -ForegroundColor Green
} catch {
$results += [PSCustomObject]@{
Server = $server
Status = "Failed"
OS = $null
LastBoot = $null
Error = $_.Exception.Message
}
Write-Host " ✗ Failed: $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "Summary:" -ForegroundColor Cyan
$results | Format-Table -AutoSize
This pattern ensures that one failing server doesn't prevent processing of remaining servers.
Exercises
Implement the classic FizzBuzz problem using PowerShell control flow:
For numbers 1 through 30:
- Print "Fizz" for multiples of 3
- Print "Buzz" for multiples of 5
- Print "FizzBuzz" for multiples of both 3 and 5
- Otherwise print the number
Implement it three different ways:
- Using
if/elseif/else - Using
switchwith script blocks - Using the pipeline with the ternary operator (PowerShell 7+)
Show Solution
# Method 1: If/ElseIf/Else
Write-Host "Method 1: If/ElseIf" -ForegroundColor Cyan
for ($i = 1; $i -le 30; $i++) {
if ($i % 15 -eq 0) {
"FizzBuzz"
} elseif ($i % 3 -eq 0) {
"Fizz"
} elseif ($i % 5 -eq 0) {
"Buzz"
} else {
$i
}
}
# Method 2: Switch with script blocks
Write-Host "`nMethod 2: Switch" -ForegroundColor Cyan
1..30 | ForEach-Object {
switch ($_) {
{$_ % 15 -eq 0} { "FizzBuzz"; break }
{$_ % 3 -eq 0} { "Fizz"; break }
{$_ % 5 -eq 0} { "Buzz"; break }
default { $_ }
}
}
# Method 3: Pipeline with ternary (PowerShell 7+)
Write-Host "`nMethod 3: Ternary" -ForegroundColor Cyan
1..30 | ForEach-Object {
$_ % 15 -eq 0 ? "FizzBuzz" :
($_ % 3 -eq 0 ? "Fizz" :
($_ % 5 -eq 0 ? "Buzz" : $_))
}
Explanation:
- Testing
$i % 15first checks for multiples of both 3 and 5 (since 15 = 3 × 5) - Order matters in the if/elseif version — most specific condition first
breakin switch prevents multiple outputs per number- Ternary operator can be nested but becomes less readable beyond 2-3 levels
Create an interactive number guessing game with these features:
- Generate a random number between 1 and 100
- Prompt the user to guess
- Give hints ("Higher" or "Lower") after each incorrect guess
- Count the number of attempts
- Display an achievement rating based on attempts:
- 1-5 attempts: "Amazing!"
- 6-7 attempts: "Great!"
- 8-10 attempts: "Good!"
- More than 10: "Keep practicing!"
- Allow the player to play multiple rounds
Show Solution
function Start-GuessingGame {
do {
# Generate target number
$target = Get-Random -Minimum 1 -Maximum 101
$attempts = 0
$guessed = $false
Write-Host ""
Write-Host "=== Number Guessing Game ===" -ForegroundColor Cyan
Write-Host "I'm thinking of a number between 1 and 100..." -ForegroundColor Cyan
Write-Host ""
# Game loop
do {
$input = Read-Host "Your guess"
# Validate input
if (-not ($input -as [int])) {
Write-Host "Please enter a valid number!" -ForegroundColor Red
continue
}
$guess = [int]$input
$attempts++
# Check guess
if ($guess -lt 1 -or $guess -gt 100) {
Write-Host "Number must be between 1 and 100!" -ForegroundColor Red
} elseif ($guess -lt $target) {
Write-Host "Higher!" -ForegroundColor Yellow
} elseif ($guess -gt $target) {
Write-Host "Lower!" -ForegroundColor Yellow
} else {
$guessed = $true
Write-Host ""
Write-Host "🎉 Correct! You got it in $attempts attempts!" -ForegroundColor Green
# Achievement rating
$rating = switch ($attempts) {
{$_ -le 5} { "Amazing! 🌟" }
{$_ -le 7} { "Great job! 👍" }
{$_ -le 10} { "Good work! 😊" }
default { "Keep practicing! 💪" }
}
Write-Host $rating -ForegroundColor Magenta
Write-Host ""
}
} until ($guessed)
# Play again?
$playAgain = Read-Host "Play again? (y/n)"
} while ($playAgain -eq 'y')
Write-Host "Thanks for playing! 👋" -ForegroundColor Cyan
}
# Start the game
Start-GuessingGame
Key techniques:
do-untilensures at least one guess attempt- Input validation prevents errors from non-numeric input
continueskips invalid input without counting as an attemptswitchwith script blocks provides flexible achievement classification- Outer
do-whileloop enables multiple game rounds
Write a script that processes a list of files with the following requirements:
- Accept an array of file paths
- For each file, attempt to read its content
- If a file is locked or temporarily unavailable, retry up to 3 times with a 2-second delay
- Track successful and failed files
- Display a summary at the end
Show Solution
function Process-FilesWithRetry {
param(
[string[]]$FilePaths,
[int]$MaxRetries = 3,
[int]$RetryDelay = 2
)
$results = @{
Success = @()
Failed = @()
TotalAttempts = 0
}
foreach ($filePath in $FilePaths) {
Write-Host "`nProcessing: $filePath" -ForegroundColor Cyan
$processed = $false
for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
$results.TotalAttempts++
try {
# Attempt to read file
$content = Get-Content $filePath -ErrorAction Stop
$lineCount = $content.Count
Write-Host " ✓ Success on attempt $attempt ($lineCount lines)" -ForegroundColor Green
$results.Success += [PSCustomObject]@{
FilePath = $filePath
Lines = $lineCount
Attempts = $attempt
}
$processed = $true
break # Success — exit retry loop
} catch {
Write-Warning " Attempt $attempt failed: $($_.Exception.Message)"
if ($attempt -lt $MaxRetries) {
Write-Host " Retrying in $RetryDelay seconds..." -ForegroundColor Yellow
Start-Sleep -Seconds $RetryDelay
}
}
}
if (-not $processed) {
Write-Host " ✗ Failed after $MaxRetries attempts" -ForegroundColor Red
$results.Failed += $filePath
}
}
# Display summary
Write-Host ""
Write-Host "=== Processing Summary ===" -ForegroundColor Cyan
Write-Host "Total files: $($FilePaths.Count)" -ForegroundColor White
Write-Host "Successful: $($results.Success.Count)" -ForegroundColor Green
Write-Host "Failed: $($results.Failed.Count)" -ForegroundColor Red
Write-Host "Total attempts: $($results.TotalAttempts)" -ForegroundColor Yellow
if ($results.Success.Count -gt 0) {
Write-Host "`nSuccessful files:" -ForegroundColor Green
$results.Success | Format-Table FilePath, Lines, Attempts -AutoSize
}
if ($results.Failed.Count -gt 0) {
Write-Host "Failed files:" -ForegroundColor Red
$results.Failed | ForEach-Object { " $_" }
}
return $results
}
# Example usage:
$testFiles = @(
"C:\Users\Public\Documents\test1.txt",
"C:\Users\Public\Documents\test2.txt",
"C:\Users\Public\Documents\test3.txt"
)
# Create test files for demonstration
$testFiles | ForEach-Object {
if (-not (Test-Path $_)) {
"Sample content" | Out-File $_
}
}
Process-FilesWithRetry -FilePaths $testFiles
Patterns demonstrated:
- Nested loops: outer
foreachfor files, innerforfor retry attempts breakexits retry loop on successtry-catchfor error handlingPSCustomObjectfor structured result tracking- Detailed progress reporting with color-coded output
- Summary statistics calculated from collected results
Summary
Control flow is the foundation of intelligent scripts:
if/elseif/elsebranches execution based on conditionsswitchprovides powerful pattern matching with wildcards, regex, arrays, and file processingforloops for counted iterations with full control over initialization and incrementforeachloops naturally iterate over collectionswhileloops repeat while a condition is true (checked before execution)do-while/do-untilguarantee at least one execution (checked after)breakexits loops early (use labels to break outer loops)continueskips to the next iteration
Master these constructs to write scripts that adapt to their environment, process data efficiently, and handle errors gracefully.
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 →