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.
$fruits = @("Apple", "Banana", "Cherry")
foreach ($fruit in $fruits) {
Write-Output "I like $fruit"
}where:
$fruitis a temporary variable representing the current item on each iteration.$fruitsis the collection being iterated: the loop runs once for every element it contains.
./foreach-basic.ps1I like Apple
I like Banana
I like Cherry$ServerPool = @("web-prod-01", "web-prod-02", "sql-prod-01")
foreach ($server in $ServerPool) {
Write-Output "Initiating health check for host: $server"
}where:
$ServerPoolis an array of strings representing target hostnames.$serveris the user-defined iteration variable representing the single item currently being processed.inis the language keyword separating the loop variable from the collection.Write-Outputoutputs the message during each pass.
pwsh ./iterate-servers.ps1Initiating health check for host: web-prod-01
Initiating health check for host: web-prod-02
Initiating health check for host: sql-prod-01Iterating Over a Range
Combine foreach with the range operator to loop a fixed number of times.
foreach ($number in 1..5) {
Write-Output "Number: $number"
}Number: 1
Number: 2
Number: 3
Number: 4
Number: 5Iterating Over a Hashtable
Looping through a hashtable requires .GetEnumerator() to access both keys and values together.
$config = @{ Server = "WEB01"; Port = 8080; Protocol = "HTTPS" }
foreach ($entry in $config.GetEnumerator()) {
Write-Output "$($entry.Key): $($entry.Value)"
}./foreach-hashtable.ps1Protocol: HTTPS
Port: 8080
Server: WEB01Capturing 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.
$Squares = foreach ($n in 1..4) {
$n * $n
}
$Squares -join ", "1, 4, 9, 16This 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 |
$numbers = 1..3
foreach ($num in $numbers) {
Write-Output "foreach: $num"
}
$numbers | ForEach-Object {
Write-Output "ForEach-Object: $_"
}./side-by-side.ps1foreach: 1
foreach: 2
foreach: 3
ForEach-Object: 1
ForEach-Object: 2
ForEach-Object: 3Practical 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:
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 -AutoSizewhere:
foreach ($ServiceName in $ServiceTargets)iterates through each string in the target collection.Get-Service -ErrorAction SilentlyContinueprevents the script from stopping if a service does not exist.$ServiceAudit = foreach (...)automatically aggregates the objects emitted during each iteration into a strongly-typedSystem.Object[]array.Format-Table -AutoSizepresents the resulting collection in a clean tabular view.
Execute the script from the console:
pwsh ./audit-service-targets.ps1ServiceName 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