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-while when the condition expresses "keep going while this is true"
  • Use do-until when 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

🏋️ Exercise 1: FizzBuzz

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:

  1. Using if/elseif/else
  2. Using switch with script blocks
  3. 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 % 15 first checks for multiples of both 3 and 5 (since 15 = 3 × 5)
  • Order matters in the if/elseif version — most specific condition first
  • break in switch prevents multiple outputs per number
  • Ternary operator can be nested but becomes less readable beyond 2-3 levels
🏋️ Exercise 2: Number Guessing Game

Create an interactive number guessing game with these features:

  1. Generate a random number between 1 and 100
  2. Prompt the user to guess
  3. Give hints ("Higher" or "Lower") after each incorrect guess
  4. Count the number of attempts
  5. Display an achievement rating based on attempts:
    • 1-5 attempts: "Amazing!"
    • 6-7 attempts: "Great!"
    • 8-10 attempts: "Good!"
    • More than 10: "Keep practicing!"
  6. 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-until ensures at least one guess attempt
  • Input validation prevents errors from non-numeric input
  • continue skips invalid input without counting as an attempt
  • switch with script blocks provides flexible achievement classification
  • Outer do-while loop enables multiple game rounds
🏋️ Exercise 3: File Processor with Retry Logic

Write a script that processes a list of files with the following requirements:

  1. Accept an array of file paths
  2. For each file, attempt to read its content
  3. If a file is locked or temporarily unavailable, retry up to 3 times with a 2-second delay
  4. Track successful and failed files
  5. 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 foreach for files, inner for for retry attempts
  • break exits retry loop on success
  • try-catch for error handling
  • PSCustomObject for 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/else branches execution based on conditions
  • switch provides powerful pattern matching with wildcards, regex, arrays, and file processing
  • for loops for counted iterations with full control over initialization and increment
  • foreach loops naturally iterate over collections
  • while loops repeat while a condition is true (checked before execution)
  • do-while/do-until guarantee at least one execution (checked after)
  • break exits loops early (use labels to break outer loops)
  • continue skips to the next iteration

Master these constructs to write scripts that adapt to their environment, process data efficiently, and handle errors gracefully.

Was this page helpful?
SR

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 →