The ForEach-Object cmdlet is a pipeline-based iteration construct that processes items one at a time as they flow through the pipeline. Unlike the foreach statement, which loads an entire collection into memory before iterating, ForEach-Object streams objects and executes a script block for each one, making it ideal for large datasets, continuous pipelines, and memory-constrained environments.
Basic Syntax
ForEach-Object is used within a pipeline, accepting input from the previous command and applying a script block to each object.
<collection> | ForEach-Object { <script block> }The cmdlet also has a built-in alias: %.
<collection> | % { <script block> }Simple Example
1..5 | ForEach-Object { Write-Output "Number: $_" }Number: 1
Number: 2
Number: 3
Number: 4
Number: 5The script block runs once for every object that arrives through the pipeline.
The $_ Automatic Variable
Inside a ForEach-Object script block, the current pipeline object is automatically stored in the $_ variable (also accessible as $PSItem). This is the primary way to reference the item being processed.
Get-Process | ForEach-Object { "Process: $($_.Name) (PID: $($_.Id))" }Process: svchost (PID: 1024)
Process: explorer (PID: 2048)
Process: pwsh (PID: 4096)
...Because $_ is a full object, you can access its properties and methods directly:
Get-ChildItem -Path C:\Windows\System32\*.dll | ForEach-Object {
"File: $($_.Name) - Size: $([math]::Round($_.Length / 1KB, 2)) KB"
}File: advapi32.dll - Size: 665.00 KB
File: kernel32.dll - Size: 740.50 KB
...Begin, Process, End Blocks
For advanced scenarios, ForEach-Object supports -Begin, -Process, and -End script blocks. This allows you to initialize resources before processing, handle each item, and clean up afterward.
<collection> | ForEach-Object -Begin {
} -Process {
} -End {
}Example: Measuring Total Size
$totalSize = 0
Get-ChildItem -Path C:\Windows\System32\*.dll | ForEach-Object -Begin {
$count = 0
} -Process {
$count++
$totalSize += $_.Length
} -End {
"Processed $count DLL files. Total size: $([math]::Round($totalSize / 1MB, 2)) MB"
}Processed 4872 DLL files. Total size: 1234.56 MBThe -Begin block runs once, -Process runs for every pipeline item, and -End runs once after all items are complete.
ForEach-Object vs. foreach Statement
Although they sound similar and can produce identical results, the foreach statement and ForEach-Object cmdlet are fundamentally different.
| Aspect | foreach Statement |
ForEach-Object Cmdlet |
|---|---|---|
| Type | Language keyword/statement | Pipeline cmdlet (alias: %) |
| 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 |
| Syntax | foreach ($item in $collection) { } |
$collection | ForEach-Object { $_ } |
| Current item variable | Custom name ($fruit, $item, 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 Example
$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: 3Pipeline Streaming and Memory Efficiency
The key advantage of ForEach-Object is its streaming behavior. As soon as an upstream cmdlet emits an object, ForEach-Object can process it, and then the object is discarded from memory. This is in contrast to the foreach statement, which must first collect all objects into an array.
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)Example: Reading a Large Log File
Get-Content -Path C:\Logs\huge.log | ForEach-Object {
if ($_ -match "ERROR") {
Write-Output $_
}
}With the foreach statement, the entire file would be loaded into memory first:
$lines = Get-Content -Path C:\Logs\huge.log
foreach ($line in $lines) {
if ($line -match "ERROR") {
Write-Output $line
}
}Practical Examples
1. Processing Files in a Directory
Get-ChildItem -Path C:\Scripts -Filter *.ps1 | ForEach-Object {
[PSCustomObject]@{
Name = $_.Name
SizeKB = [math]::Round($_.Length / 1KB, 2)
LastWrite = $_.LastWriteTime.ToString("yyyy-MM-dd")
}
} | Format-Table -AutoSize2. Filtering and Transforming Data
Get-Service | ForEach-Object {
if ($_.Status -eq "Running") {
[PSCustomObject]@{
ServiceName = $_.Name
DisplayName = $_.DisplayName
State = "Active"
}
}
} | Select-Object -First 53. Using with Other Cmdlets
Get-Process | Where-Object { $_.CPU -gt 100 } | ForEach-Object {
"$($_.ProcessName) is using $([math]::Round($_.CPU, 2)) seconds of CPU time"
}Performance Considerations
- In-memory collections: The
foreachstatement is generally faster because it avoids pipeline overhead. For small to medium collections already in memory, preferforeach. - Streaming data:
ForEach-Objectis slower per item due to pipeline parameter binding, but it uses far less memory and can process items as they arrive. For large files, network streams, or cmdlet output that you don't need to store,ForEach-Objectis the clear winner. - Break/Continue: In
ForEach-Object,breakexits the entire script (not just the loop), andcontinueacts likereturnfor the current item. Theforeachstatement supports properbreakandcontinuesemantics.