Skip to content

PowerShell ForEach-Object

How to use PowerShell's ForEach-Object cmdlet for efficient pipeline-based iteration, memory optimization, and processing large datasets with $_ and script blocks.

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

Consolepowershell
1..5 | ForEach-Object { Write-Output "Number: $_" }
Outputplaintext
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5

The 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.

Consolepowershell
Get-Process | ForEach-Object { "Process: $($_.Name) (PID: $($_.Id))" }
Outputplaintext
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:

Consolepowershell
Get-ChildItem -Path C:\Windows\System32\*.dll | ForEach-Object {
    "File: $($_.Name) - Size: $([math]::Round($_.Length / 1KB, 2)) KB"
}
Outputplaintext
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

Consolepowershell
$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"
}
Outputplaintext
Processed 4872 DLL files. Total size: 1234.56 MB

The -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

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

Pipeline 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

Consolepowershell
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

Consolepowershell
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 -AutoSize

2. Filtering and Transforming Data

Consolepowershell
Get-Service | ForEach-Object {
    if ($_.Status -eq "Running") {
        [PSCustomObject]@{
            ServiceName = $_.Name
            DisplayName = $_.DisplayName
            State       = "Active"
        }
    }
} | Select-Object -First 5

3. Using with Other Cmdlets

Consolepowershell
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 foreach statement is generally faster because it avoids pipeline overhead. For small to medium collections already in memory, prefer foreach.
  • Streaming data: ForEach-Object is 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-Object is the clear winner.
  • Break/Continue: In ForEach-Object, break exits the entire script (not just the loop), and continue acts like return for the current item. The foreach statement supports proper break and continue semantics.
Navigation