Skip to content

Loops

Note

Learn how to repeat actions using different types of loops in PowerShell.

Overview

Loops let you repeat code multiple times without writing it over and over. You use loops when you need to process each item in a collection, repeat an action until a condition is met, or perform an operation a specific number of times. PowerShell has several types of loops, each suited for different situations.

Loop Types Overview

foreach     # Best for: Iterating through a collection (most common)
ForEach-Object  # Best for: Pipeline operations
for         # Best for: Counting with specific start/end/increment
while       # Best for: Loop while condition is true (check first)
do-while    # Best for: Loop at least once, then while condition is true
do-until    # Best for: Loop at least once, then until condition is true

Key Points

  • foreach is best for collections (arrays, file lists, etc.)
  • ForEach-Object works in the pipeline with |
  • for is best when you need precise control over iteration
  • Use break to exit a loop early
  • Use continue to skip to the next iteration

ForEach Loop (Most Common)

Basic Syntax

foreach ($item in $collection) {
    # Code runs once for each item
    # $item contains the current item
}

Common Use Cases

Iterate through an array:

$colors = "Red", "Green", "Blue"

foreach ($color in $colors) {
    Write-Output "Current color: $color"
}

Output:

Current color: Red
Current color: Green
Current color: Blue

Loop through a range of numbers:

# Use range operator (..) with foreach
foreach ($i in 1..10) {
    Write-Output $i
}

# Works with letters too
foreach ($letter in 'a'..'e') {
    Write-Output $letter
}

Output:

1
2
3
4
5
6
7
8
9
10

Process files in a directory:

$files = Get-ChildItem -Path C:\Temp -Filter *.txt

foreach ($file in $files) {
    Write-Output "File: $($file.Name) - Size: $($file.Length) bytes"

    # Do something with each file
    $content = Get-Content -Path $file.FullName
    Write-Output "Lines: $($content.Count)"
}

Loop through hash table:

$servers = @{
    "SQL01" = "192.168.1.10"
    "WEB01" = "192.168.1.20"
    "FILE01" = "192.168.1.30"
}

foreach ($server in $servers.GetEnumerator()) {
    Write-Output "Server: $($server.Key) - IP: $($server.Value)"
    Test-Connection -ComputerName $server.Value -Count 1 -Quiet
}

ForEach-Object (Pipeline)

Basic Syntax

$collection | ForEach-Object {
    # $_ or $PSItem contains the current item
}

# Shorter alias version (common but not recommended for scripts)
$collection | % { }

When to Use

Use ForEach-Object when you're already in a pipeline:

# Process each process returned by Get-Process
Get-Process | ForEach-Object {
    Write-Output "$($_.Name) is using $($_.WS / 1MB) MB"
}

# Convert file sizes to MB
Get-ChildItem C:\Temp | ForEach-Object {
    [PSCustomObject]@{
        Name = $_.Name
        SizeMB = [math]::Round($_.Length / 1MB, 2)
    }
}

# Restart multiple services
"Spooler", "W32Time" | ForEach-Object {
    Restart-Service -Name $_ -Force
}

For Loop (Counter-Based)

Basic Syntax

for ($i = start; $i -condition end; $i = increment) {
    # Code runs while condition is true
}

Common Use Cases

Count from 1 to 10:

for ($i = 1; $i -le 10; $i++) {
    Write-Output "Count: $i"
}

Count backwards:

for ($i = 10; $i -ge 1; $i--) {
    Write-Output "Countdown: $i"
}
Write-Output "Liftoff!"

Skip by 2 (even numbers):

for ($i = 0; $i -le 20; $i += 2) {
    Write-Output $i  # 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20
}

Access array by index:

$fruits = "Apple", "Banana", "Cherry", "Date"

for ($i = 0; $i -lt $fruits.Count; $i++) {
    Write-Output "Item $i : $($fruits[$i])"
}

Output:

Item 0: Apple
Item 1: Banana
Item 2: Cherry
Item 3: Date

While Loop

Basic Syntax

while (condition) {
    # Code runs while condition is true
    # Checks condition BEFORE each iteration
}

Common Use Cases

Wait for a condition:

$service = Get-Service -Name "Spooler"

while ($service.Status -ne "Running") {
    Write-Output "Waiting for service to start..."
    Start-Sleep -Seconds 2
    $service.Refresh()  # Update service status
}

Write-Output "Service is now running!"

Process until variable changes:

$count = 0
$maxAttempts = 5

while ($count -lt $maxAttempts) {
    Write-Output "Attempt $($count + 1) of $maxAttempts"

    # Try something
    $success = Test-Connection -ComputerName "Server01" -Count 1 -Quiet

    if ($success) {
        Write-Output "Connection successful!"
        break  # Exit loop early
    }

    $count++
    Start-Sleep -Seconds 5
}

Do-While Loop

Basic Syntax

do {
    # Code runs at least once
    # Then repeats while condition is true
} while (condition)

When to Use

Use when you need to run code at least once before checking the condition:

# Menu system that runs at least once
do {
    Write-Output "`n===== Menu ====="
    Write-Output "1. Option One"
    Write-Output "2. Option Two"
    Write-Output "3. Exit"

    $choice = Read-Host "Enter choice"

    switch ($choice) {
        "1" { Write-Output "You chose option 1" }
        "2" { Write-Output "You chose option 2" }
        "3" { Write-Output "Exiting..."; break }
        default { Write-Output "Invalid choice" }
    }

} while ($choice -ne "3")

Do-Until Loop

Basic Syntax

do {
    # Code runs at least once
    # Then repeats until condition becomes true
} until (condition)

Example

$attempts = 0

do {
    $attempts++
    Write-Output "Attempt #$attempts"

    $result = Test-Connection -ComputerName "Server01" -Count 1 -Quiet

    if (-not $result) {
        Start-Sleep -Seconds 5
    }

} until ($result -eq $true -or $attempts -ge 5)

if ($result) {
    Write-Output "Connected successfully!"
} else {
    Write-Output "Failed after $attempts attempts"
}

Loop Control

Break (Exit Loop)

# Exit loop completely when condition is met
foreach ($number in 1..100) {
    if ($number -eq 10) {
        Write-Output "Found 10, stopping!"
        break  # Exits the loop
    }
    Write-Output $number
}
# Output: 1, 2, 3, 4, 5, 6, 7, 8, 9, Found 10, stopping!

Continue (Skip to Next Iteration)

# Skip certain items
foreach ($number in 1..10) {
    if ($number % 2 -eq 0) {
        continue  # Skip even numbers
    }
    Write-Output $number
}
# Output: 1, 3, 5, 7, 9 (odd numbers only)

Nested Loops with Break

# Break out of nested loops
$found = $false

foreach ($i in 1..5) {
    foreach ($j in 1..5) {
        if ($i * $j -eq 12) {
            Write-Output "Found: $i x $j = 12"
            $found = $true
            break  # Exits inner loop only
        }
    }

    if ($found) {
        break  # Exits outer loop
    }
}

Real-World Examples

Example: Retry Logic with Exponential Backoff

Scenario: Try to connect to a server with increasing wait times between attempts

$maxRetries = 5
$retryCount = 0
$success = $false
$serverName = "Server01"

while ($retryCount -lt $maxRetries -and -not $success) {
    $retryCount++
    Write-Output "Attempt $retryCount of $maxRetries..."

    $success = Test-Connection -ComputerName $serverName -Count 1 -Quiet

    if (-not $success) {
        # Exponential backoff: 2, 4, 8, 16, 32 seconds
        $waitTime = [math]::Pow(2, $retryCount)
        Write-Output "Failed. Waiting $waitTime seconds..."
        Start-Sleep -Seconds $waitTime
    }
}

if ($success) {
    Write-Output "Successfully connected to $serverName"
} else {
    Write-Output "Failed to connect after $maxRetries attempts"
}

Example: Batch Processing Files

Scenario: Process files in batches to avoid overwhelming the system

$files = Get-ChildItem -Path C:\Logs -Filter *.log
$batchSize = 10
$processedCount = 0

for ($i = 0; $i -lt $files.Count; $i += $batchSize) {
    # Get current batch
    $endIndex = [math]::Min($i + $batchSize - 1, $files.Count - 1)
    $batch = $files[$i..$endIndex]

    Write-Output "Processing batch: files $($i + 1) to $($endIndex + 1)"

    foreach ($file in $batch) {
        # Process each file in the batch
        $content = Get-Content -Path $file.FullName
        # Do something with content
        $processedCount++
    }

    # Optional: pause between batches
    if ($i + $batchSize -lt $files.Count) {
        Write-Output "Pausing before next batch..."
        Start-Sleep -Seconds 2
    }
}

Write-Output "Processed $processedCount files in total"

Example: Monitor Until Condition Met

Scenario: Watch disk space and alert when it drops below threshold

$threshold = 20  # GB
$checkInterval = 300  # 5 minutes in seconds

Write-Output "Starting disk space monitor..."
Write-Output "Alert threshold: $threshold GB"

while ($true) {
    $drive = Get-PSDrive C
    $freeSpaceGB = [math]::Round($drive.Free / 1GB, 2)

    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    Write-Output "[$timestamp] Free space: $freeSpaceGB GB"

    if ($freeSpaceGB -lt $threshold) {
        Write-Output "ALERT: Free space below threshold!"
        # Could send email, write to event log, etc.
        break
    }

    # Wait before next check
    Start-Sleep -Seconds $checkInterval
}

Performance Considerations

# SLOW: Using += in a loop creates new array each time
$results = @()
foreach ($i in 1..10000) {
    $results += $i  # Creates new array 10,000 times!
}

# FAST: Use ArrayList for growing collections
$results = [System.Collections.ArrayList]@()
foreach ($i in 1..10000) {
    $results.Add($i) | Out-Null  # Modifies existing list
}

# FAST: Pre-size array if you know the count
$results = New-Object int[] 10000
for ($i = 0; $i -lt 10000; $i++) {
    $results[$i] = $i
}

# FAST: Use pipeline when possible
$results = 1..10000 | ForEach-Object { $_ }

Tips & Tricks

Use Range Operator for Simple Sequences

# Instead of this:
for ($i = 1; $i -le 10; $i++) {
    Write-Output $i
}

# Use this:
foreach ($i in 1..10) {
    Write-Output $i
}

# Or even simpler:
1..10 | ForEach-Object { Write-Output $_ }

ForEach vs ForEach-Object

# ForEach statement (faster, no pipeline overhead)
foreach ($item in $collection) {
    # Process $item
}

# ForEach-Object cmdlet (slower, but works in pipeline)
$collection | ForEach-Object {
    # Process $_
}

# Use foreach when you have a collection variable
# Use ForEach-Object when you're in a pipeline

Label Loops for Complex Break/Continue

:outerLoop foreach ($i in 1..5) {
    foreach ($j in 1..5) {
        if ($i * $j -eq 12) {
            break outerLoop  # Breaks out of BOTH loops
        }
    }
}

Infinite Loops

# This loop never ends!
while ($true) {
    Write-Output "Forever..."
}

# Always have an exit condition
$count = 0
while ($true) {
    $count++
    if ($count -ge 10) {
        break  # Exit loop
    }
}

Modifying Collection While Looping

# DON'T do this - modifying array while iterating
$numbers = 1..10
foreach ($num in $numbers) {
    if ($num -eq 5) {
        $numbers += 11  # BAD: Modifying source array
    }
}

# DO this - create new array with modifications
$numbers = 1..10
$newNumbers = foreach ($num in $numbers) {
    $num
    if ($num -eq 5) {
        11  # Add to output
    }
}

Loop Performance with Pipeline

# SLOW: Calling cmdlet inside loop
foreach ($i in 1..1000) {
    Get-Process | Where-Object {$_.Id -eq $i}
}

# FAST: Get data once, then loop
$processes = Get-Process
foreach ($i in 1..1000) {
    $processes | Where-Object {$_.Id -eq $i}
}

Additional Resources