Skip to content

PowerShell Foreach Loop

How to use PowerShell's foreach loop to iterate through arrays, hashtables, and collections efficiently, with syntax examples and comparisons to ForEach-Object.

The foreach loop (also referred to as the foreach statement) is a collection-driven control flow structure that iterates sequentially through an enumerable array, list, hashtable, or object collection. It loads the entire collection into system memory upfront and executes an enclosed script block once for every individual element. It's the most readable choice when you already have a complete collection stored in a variable and simply need to process each element.

The foreach Statement Syntax

The foreach statement defines an iteration variable that temporarily holds the current element, followed by the in keyword and the target collection:

Syntax Structure

foreach ($<item> in $<collection>) {
}

On each pass, PowerShell assigns the next item in the collection to $<item> and executes the statements inside the curly braces ({}). When no elements remain, the loop terminates and execution continues with the subsequent command.

foreach-basic.ps1powershell
$fruits = @("Apple", "Banana", "Cherry")

foreach ($fruit in $fruits) {
    Write-Output "I like $fruit"
}

where:

  • $fruit is a temporary variable representing the current item on each iteration.
  • $fruits is the collection being iterated: the loop runs once for every element it contains.
Consolepowershell
./foreach-basic.ps1
Outputplaintext
I like Apple
I like Banana
I like Cherry
iterate-servers.ps1powershell
$ServerPool = @("web-prod-01", "web-prod-02", "sql-prod-01")

foreach ($server in $ServerPool) {
    Write-Output "Initiating health check for host: $server"
}

where:

  • $ServerPool is an array of strings representing target hostnames.
  • $server is the user-defined iteration variable representing the single item currently being processed.
  • in is the language keyword separating the loop variable from the collection.
  • Write-Output outputs the message during each pass.
Consolepowershell
pwsh ./iterate-servers.ps1
Outputplaintext
Initiating health check for host: web-prod-01
Initiating health check for host: web-prod-02
Initiating health check for host: sql-prod-01

Iterating Over a Range

Combine foreach with the range operator to loop a fixed number of times.

Consolepowershell
foreach ($number in 1..5) {
    Write-Output "Number: $number"
}
Outputplaintext
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5

Iterating Over a Hashtable

Looping through a hashtable requires .GetEnumerator() to access both keys and values together.

foreach-hashtable.ps1powershell
$config = @{ Server = "WEB01"; Port = 8080; Protocol = "HTTPS" }

foreach ($entry in $config.GetEnumerator()) {
    Write-Output "$($entry.Key): $($entry.Value)"
}
Consolepowershell
./foreach-hashtable.ps1
Outputplaintext
Protocol: HTTPS
Port: 8080
Server: WEB01

Capturing Loop Output Directly

In PowerShell, statements like foreach loops automatically output objects that can be directly captured into an array variable. This eliminates the need for manual .Add() calls or array concatenation using the += operator.

Consolepowershell
$Squares = foreach ($n in 1..4) {
    $n * $n
}

$Squares -join ", "
Outputplaintext
1, 4, 9, 16

This pattern is significantly faster than using $Squares += ($n * $n) inside the loop body because it avoids repeatedly reallocating arrays in memory.

foreach Statement vs. ForEach-Object Cmdlet

Although they share similar names and can produce identical results, the foreach statement and the ForEach-Object cmdlet are architecturally different in how they manage memory, pipeline objects, and execution speed.

foreach Statement (In-Memory Processing):
[ Entire Collection ] ---> Loaded into RAM upfront ---> Processes items in tight loop (Fast)

ForEach-Object Cmdlet (Pipeline Streaming):
[ Producer Cmdlet ] === (Item 1) ===> [ ForEach-Object ] === (Processed 1) ===> Downstream
                    === (Item 2) ===> [ ForEach-Object ] === (Processed 2) ===> Downstream
                    (Streams one item at a time; low memory footprint)

Architectural Comparison

Dimension foreach Statement ForEach-Object Cmdlet
Language Role Built-in language keyword/statement Compiled pipeline cmdlet (ForEach-Object, alias: %)
Type Language keyword/statement Pipeline cmdlet
Data Ingestion Requires the entire collection to exist in memory before execution begins Streams items one at a time directly through the pipeline (|)
Input source A collection already stored in a variable Objects streamed through the pipeline (|)
Memory usage Loads the entire collection into memory first Processes one object at a time as it flows through
Execution Speed Fastest (avoids pipeline parameter binding and cmdlet invocation overhead) Slower (incurs pipeline marshalling overhead for each item)
Memory Footprint High (entire dataset must fit in available RAM) Low (only the active object resides in pipeline memory)
Syntax foreach ($item in $collection) { } $collection | ForEach-Object { $_ }
Current item variable Custom name ($fruit, $item, $server, etc.) Always $_ (or $PSItem)
Performance (large data) Generally faster for in-memory collections Better suited to streaming, large, or piped data
Loop Control Supports standard break and continue keywords break can terminate the entire calling script; continue functions like return
side-by-side.ps1powershell
$numbers = 1..3

foreach ($num in $numbers) {
    Write-Output "foreach: $num"
}

$numbers | ForEach-Object {
    Write-Output "ForEach-Object: $_"
}
Consolepowershell
./side-by-side.ps1
Outputplaintext
foreach: 1
foreach: 2
foreach: 3
ForEach-Object: 1
ForEach-Object: 2
ForEach-Object: 3

Practical Example: Batch Service Health and Configuration Audit

The following script iterates through a pre-defined list of system service names, queries their live configuration, evaluates whether each is operating normally, and collects the results into an array of custom diagnostic objects:

audit-service-targets.ps1powershell
param ()

$ServiceTargets = @("wuauserv", "Spooler", "LanmanServer", "NonExistentService")

$ServiceAudit = foreach ($ServiceName in $ServiceTargets) {
    $ServiceObj = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue

    if ($null -eq $ServiceObj) {
        [PSCustomObject]@{
            ServiceName   = $ServiceName
            DisplayName   = "NOT INSTALLED"
            Status        = "Missing"
            StartType     = "Unknown"
            RequiresAlert = $true
        }
    }
    else {
        $isDegraded = ($ServiceObj.StartType -eq "Automatic" -and $ServiceObj.Status -ne "Running")

        [PSCustomObject]@{
            ServiceName   = $ServiceObj.ServiceName
            DisplayName   = $ServiceObj.DisplayName
            Status        = $ServiceObj.Status
            StartType     = $ServiceObj.StartType
            RequiresAlert = $isDegraded
        }
    }
}

$ServiceAudit | Format-Table -AutoSize

where:

  • foreach ($ServiceName in $ServiceTargets) iterates through each string in the target collection.
  • Get-Service -ErrorAction SilentlyContinue prevents the script from stopping if a service does not exist.
  • $ServiceAudit = foreach (...) automatically aggregates the objects emitted during each iteration into a strongly-typed System.Object[] array.
  • Format-Table -AutoSize presents the resulting collection in a clean tabular view.

Execute the script from the console:

Consolepowershell
pwsh ./audit-service-targets.ps1
Outputplaintext
ServiceName        DisplayName     Status StartType RequiresAlert
-----------        -----------     ------ --------- -------------
wuauserv           Windows Update Running    Manual         False
Spooler            Print Spooler  Running Automatic         False
LanmanServer       Server         Running Automatic         False
NonExistentService NOT INSTALLED  Missing   Unknown          True
Navigation