Skip to main content

PowerShell ForEach-Object: Complete Guide with Examples [2024]

9 min read
powershell foreach-object loops pipeline foreach loop iteration collections windows

Master the PowerShell ForEach-Object cmdlet—the most powerful and flexible way to iterate through collections in the pipeline. This comprehensive guide covers basic and advanced usage, performance optimization, and real-world scenarios.

Table of Contents

  1. What is ForEach-Object?
  2. Why Use ForEach-Object?
  3. ForEach-Object Syntax
  4. Basic Pipeline Iteration
  5. Script Block Variables
  6. Processing Script Block
  7. Begin and End Script Blocks
  8. Processing Collections
  9. Working with Objects
  10. Advanced: Parallel Processing
  11. Performance Comparison
  12. Common Mistakes
  13. Best Practices
  14. Troubleshooting
  15. FAQs
  16. Real-World Use Cases
  17. Conclusion

What is ForEach-Object?

The ForEach-Object cmdlet iterates through each item in a collection and executes a script block for every item. Unlike traditional loops, ForEach-Object is pipeline-aware, meaning it processes items as they flow through the pipeline, making it ideal for working with PowerShell objects and cmdlet output.

Key Characteristics:

  • ✅ Pipeline-friendly (processes one item at a time)
  • ✅ Works with any collection type
  • ✅ Can access current item properties directly
  • ✅ Supports parallel execution (PowerShell 7.0+)
  • ✅ Memory efficient for large datasets
  • ✅ Perfect for transforming cmdlet output

Why Use ForEach-Object?

When to Choose ForEach-Object:

  1. Pipeline Processing: Working with cmdlet output that streams one item at a time
  2. Object Transformation: Modifying or extracting properties from objects
  3. Side Effects: Executing commands for each item (moving files, updating records)
  4. Filtering and Mapping: Combined with Where-Object for data manipulation
  5. Parallel Processing: Processing large collections in parallel (PowerShell 7+)
  6. Large Collections: Memory efficient when working with millions of items

Comparison with Other Loop Methods:

FeatureForEach-Objectforeach Loopfor Loop
Pipeline-aware✅ Yes❌ No❌ No
Memory usageMost efficientModerateModerate
One-liner✅ EasyHarderHarder
Parallel support✅ Yes (PS7+)❌ No❌ No
Access propertiesVia $_Via variableVia index
Best forCmdlet outputComplete collectionsCounter loops
ReadabilityHighHighModerate

ForEach-Object Syntax {#syntax}

Basic Pipeline Syntax

<collection> | ForEach-Object {
    # Script block to execute for each item
}

Full Syntax with All Parameters

ForEach-Object -InputObject <PSObject[]>
               -Begin <scriptblock>
               -Process <scriptblock>
               -End <scriptblock>
               [-RemainingScripts <scriptblock[]>]

Using Named Parameters

Get-Service | ForEach-Object -Process {
    Write-Output "Service: $($_.Name)"
}

Shorthand Notation (Alias)

Get-Service | % { Write-Output "Service: $($_.Name)" }

Parameter Explanations:

  • -InputObject: The collection to iterate over (usually provided via pipeline)
  • -Begin: Runs once before processing any items
  • -Process: Runs for each item in the collection (main logic)
  • -End: Runs once after all items are processed

Basic Pipeline Iteration {#basic-pipeline}

Example 1: Simple String Output

@("Apple", "Banana", "Cherry") | ForEach-Object {
    Write-Output "Fruit: $_"
}

Output:

Fruit: Apple
Fruit: Banana
Fruit: Cherry

Explanation: The $_ variable represents the current item being processed.

Example 2: Processing Cmdlet Output

Get-Process | ForEach-Object {
    Write-Output "Process: $($_.Name), Memory: $([Math]::Round($_.WorkingSet / 1MB, 2)) MB"
}

Output:

Process: svchost, Memory: 45.32 MB
Process: explorer, Memory: 312.15 MB
Process: chrome, Memory: 1024.78 MB
...

Example 3: Using -Begin and -End

$services = @("BITS", "wuauserv", "WinDefend")

$services | ForEach-Object -Begin {
    Write-Output "=== Service Status Report ==="
    $count = 0
} -Process {
    $svc = Get-Service -Name $_ -ErrorAction SilentlyContinue
    if ($svc) {
        Write-Output "$($_.PadRight(20)) : $($svc.Status)"
        $count++
    }
} -End {
    Write-Output "=== Total Services Checked: $count ==="
}

Output:

=== Service Status Report ===
BITS                 : Stopped
wuauserv             : Running
WinDefend            : Running
=== Total Services Checked: 3 ===

Script Block Variables {#script-block-variables}

The $_ Automatic Variable

The $_ variable always represents the current object being processed.

# Example: Access properties of $_
Get-Service | ForEach-Object {
    $serviceName = $_.Name
    $status = $_.Status
    Write-Output "Name: $serviceName, Status: $status"
}

The $PSItem Variable (Alias for $_)

In PowerShell 3.0+, you can use $PSItem instead of $_:

Get-Service | ForEach-Object {
    Write-Output "Service: $($PSItem.Name)"
}

Accessing Index with Automatic Variable

@("First", "Second", "Third") | ForEach-Object {
    Write-Output "Item $($PSItem._): $_"
}

Note: To get the index, you need a different approach (see advanced section).


Processing Script Block {#processing-script-block}

Basic -Process Block

1..5 | ForEach-Object -Process {
    $square = $_ * $_
    Write-Output "Number: $_, Square: $square"
}

Output:

Number: 1, Square: 1
Number: 2, Square: 4
Number: 3, Square: 9
Number: 4, Square: 16
Number: 5, Square: 25

Conditional Logic in Process Block

Get-Service | ForEach-Object -Process {
    if ($_.Status -eq "Running") {
        Write-Output "$($_.Name) is RUNNING"
    } else {
        Write-Output "$($_.Name) is STOPPED"
    }
}

Creating New Objects

Get-Process | ForEach-Object {
    [PSCustomObject]@{
        ProcessName = $_.Name
        ProcessID = $_.Id
        MemoryMB = [Math]::Round($_.WorkingSet / 1MB, 2)
        CPUTime = $_.TotalProcessorTime
    }
} | Format-Table -AutoSize

Begin and End Script Blocks {#begin-end-blocks}

Example: Counting and Summarizing

$files = Get-ChildItem -Path "C:\Temp" -File

$files | ForEach-Object -Begin {
    Write-Output "Processing files..."
    $totalSize = 0
    $fileCount = 0
} -Process {
    $totalSize += $_.Length
    $fileCount++
} -End {
    Write-Output "Files processed: $fileCount"
    Write-Output "Total size: $([Math]::Round($totalSize / 1MB, 2)) MB"
}

Example: Building Collections

$results = @()

Get-Service | ForEach-Object -Begin {
    Write-Output "Collecting service data..."
} -Process {
    $results += [PSCustomObject]@{
        Name = $_.Name
        Status = $_.Status
        DisplayName = $_.DisplayName
    }
} -End {
    Write-Output "Collected $($results.Count) services"
    $results | Export-Csv -Path "C:\Temp\services.csv" -NoTypeInformation
}

Processing Collections {#processing-collections}

Arrays

$numbers = @(10, 20, 30, 40, 50)

$numbers | ForEach-Object {
    Write-Output "Number: $_, Doubled: $($_ * 2)"
}

Hashtables

$servers = @(
    @{Name = "Server1"; IP = "192.168.1.1"},
    @{Name = "Server2"; IP = "192.168.1.2"},
    @{Name = "Server3"; IP = "192.168.1.3"}
)

$servers | ForEach-Object {
    Write-Output "Server: $($_.Name) - IP: $($_.IP)"
}

Custom Objects

$users = @(
    [PSCustomObject]@{Name = "John"; Age = 28; Department = "IT"},
    [PSCustomObject]@{Name = "Jane"; Age = 32; Department = "HR"},
    [PSCustomObject]@{Name = "Bob"; Age = 25; Department = "IT"}
)

$users | Where-Object {$_.Department -eq "IT"} | ForEach-Object {
    Write-Output "IT Employee: $($_.Name), Age: $($_.Age)"
}

Working with Objects {#working-with-objects}

Extracting Properties

Get-Service | ForEach-Object {
    [PSCustomObject]@{
        ServiceName = $_.Name
        Status = $_.Status
        StartType = $_.StartType
    }
}

Calling Object Methods

Get-Service -Name "BITS" | ForEach-Object {
    try {
        $_.Start()
        Write-Output "Started service: $($_.Name)"
    } catch {
        Write-Output "Failed to start service: $_"
    }
}

Modifying Object Properties

Get-Item -Path "C:\Temp\*.txt" | ForEach-Object {
    $_.LastWriteTime = Get-Date
    Write-Output "Updated: $($_.Name)"
}

Advanced: Parallel Processing {#parallel-processing}

Available in PowerShell 7.0 and later

Basic Parallel Iteration

1..10 | ForEach-Object -Parallel {
    Write-Output "Processing number: $_"
    Start-Sleep -Seconds 1
}

Using Throttle Limit

1..100 | ForEach-Object -Parallel -ThrottleLimit 4 {
    $result = $_ * $_
    Write-Output "Number: $_, Square: $result"
}

Explanation: -ThrottleLimit 4 means 4 items will be processed simultaneously.

Accessing Parent Scope Variables

$multiplier = 10

1..5 | ForEach-Object -Parallel {
    # Must use $using: prefix to access parent scope
    $result = $_ * $using:multiplier
    Write-Output "$_ * $using:multiplier = $result"
}

Output:

1 * 10 = 10
2 * 10 = 20
3 * 10 = 30
4 * 10 = 40
5 * 10 = 50

Parallel Processing with Throttle

$files = Get-ChildItem -Path "C:\LargeDirectory" -File

$files | ForEach-Object -Parallel -ThrottleLimit 8 {
    # Process files in parallel (max 8 at a time)
    $hash = Get-FileHash -Path $_.FullName -Algorithm SHA256
    Write-Output "File: $($_.Name), Hash: $($hash.Hash.Substring(0, 16))..."
} -Verbose

Performance Comparison {#performance-comparison}

Benchmark: Iterating 10,000 Numbers

# Test 1: ForEach-Object
Measure-Command {
    1..10000 | ForEach-Object { $_ * $_ } | Out-Null
} | Select-Object -Property Milliseconds

# Test 2: foreach loop
Measure-Command {
    foreach ($i in 1..10000) { $i * $i }
} | Select-Object -Property Milliseconds

# Test 3: for loop
Measure-Command {
    for ($i = 1; $i -le 10000; $i++) { $i * $i }
} | Select-Object -Property Milliseconds

Typical Results (on modern system):

  • ForEach-Object: ~150-200ms
  • foreach loop: ~50-100ms (faster, no pipeline overhead)
  • for loop: ~40-80ms (fastest, but less flexible)

Conclusion: ForEach-Object has slight overhead due to pipeline, but this is negligible for normal workloads and worth it for pipeline integration.


Common Mistakes {#common-mistakes}

❌ Mistake 1: Forgetting the Pipeline

# WRONG: ForEach-Object expects pipeline input
ForEach-Object { Write-Output $_ }

# RIGHT: Pipe collection to ForEach-Object
@(1, 2, 3) | ForEach-Object { Write-Output $_ }

❌ Mistake 2: Using Array Index Instead of $_

# WRONG: $i doesn't exist in ForEach-Object
@("a", "b", "c") | ForEach-Object { Write-Output "Index: $i" }

# RIGHT: Use counter with -Begin/-Process
$count = 0
@("a", "b", "c") | ForEach-Object -Begin {
    $count = 0
} -Process {
    Write-Output "Index: $count, Value: $_"
    $count++
}

❌ Mistake 3: Assigning to Collection in Process Block

# SLOW: Creates new array each iteration
$results = @()
1..1000 | ForEach-Object { $results += $_ }

# BETTER: Use -End block to finalize
$results = @()
1..1000 | ForEach-Object -Process {
    $results += $_
} -End {
    Write-Output "Collected: $($results.Count) items"
}

# BEST: Use ArrayList for better performance
$results = [System.Collections.ArrayList]::new()
1..1000 | ForEach-Object { $results.Add($_) | Out-Null }

❌ Mistake 4: Forgetting to Output in Process Block

# WRONG: Nothing happens (no output)
@(1, 2, 3) | ForEach-Object { $_ * 2 }

# RIGHT: Explicitly output
@(1, 2, 3) | ForEach-Object { Write-Output ($_ * 2) }

# ALSO RIGHT: PowerShell automatically outputs the result
@(1, 2, 3) | ForEach-Object { $_ * 2 }  # This actually works due to implicit output

❌ Mistake 5: Not Handling Errors

# WRONG: Script stops on first error
Get-Content -Path @("file1.txt", "file2.txt", "badfile.txt") | ForEach-Object { $_ }

# RIGHT: Add error handling
Get-Content -Path @("file1.txt", "file2.txt", "badfile.txt") -ErrorAction SilentlyContinue | ForEach-Object { $_ }

Best Practices {#best-practices}

Use ForEach-Object for:

  • Processing cmdlet output
  • One-liner operations
  • Parallel processing (PS7+)
  • Transforming objects

Use foreach loop for:

  • Complete arrays already in memory
  • Complex multi-line logic
  • Performance-critical code
  • Better readability with long scripts

Use for loop for:

  • Counter-based iterations
  • Breaking/continuing with specific conditions
  • Backwards iteration
  • Complex index-based logic

Performance Tips:

  • Use ArrayList instead of array += for large collections
  • Use -Begin/-End blocks efficiently
  • Consider parallel processing for I/O-bound operations
  • Avoid creating arrays in -Process block

Code Quality:

  • Use descriptive variable names
  • Add comments for complex logic
  • Use error handling with try-catch
  • Test with real data volumes

Troubleshooting {#troubleshooting}

Issue: “Cannot find a parameter that matches ’$_’”

  • Cause: Using $_ outside of ForEach-Object context
  • Solution: Ensure you’re using $_ inside the script block

Issue: “The property ‘PropertyName’ cannot be found on this object”

  • Cause: Accessing non-existent property
  • Solution: Verify property exists with Get-Member or use -ErrorAction SilentlyContinue

Issue: Parallel ForEach-Object not working

  • Cause: Using PowerShell version < 7.0
  • Solution: Upgrade to PowerShell 7.0+ or use foreach loop alternative

Issue: Performance is slow

  • Cause: Too much work in each iteration or inefficient data structures
  • Solution: Profile with Measure-Command, use ArrayList instead of array +=

Issue: Can’t access parent scope variables in -Parallel

  • Cause: Missing $using: prefix
  • Solution: Use $using:variableName for variables from parent scope

FAQs {#faqs}

Q: What’s the difference between ForEach-Object and foreach loop?

A: ForEach-Object is pipeline-aware and processes items as they flow through the pipeline. It’s ideal for processing cmdlet output one item at a time. The foreach loop expects an entire collection in memory and is better for complex logic blocks.

Q: How do I get the index of the current item in ForEach-Object?

A: Use a counter variable in -Begin and -Process blocks:

$index = 0
$items | ForEach-Object -Begin { $index = 0 } -Process { Write-Output "Index: $index"; $index++ }

Q: Can I use ForEach-Object with strings?

A: Yes, a string is treated as a collection of characters:

"PowerShell" | ForEach-Object { Write-Output $_ }  # Outputs each character

Q: When should I use -Parallel?

A: Use parallel processing for I/O-bound operations (network requests, file operations) or CPU-intensive tasks on multi-core systems. Avoid for simple operations due to overhead.

Q: How do I break or continue in ForEach-Object?

A: You can use return (acts like continue) or throw (acts like break):

1..10 | ForEach-Object {
    if ($_ -eq 5) { return }  # Skip this item
    Write-Output $_
}

Q: What’s the memory impact of processing large collections?

A: ForEach-Object is memory-efficient because it processes items one at a time rather than loading the entire collection into memory.

Q: Can I use ForEach-Object with -Confirm or -WhatIf?

A: Yes, these parameters work with actions inside ForEach-Object:

Get-Item "C:\Temp\*" | ForEach-Object { Remove-Item -Path $_ -WhatIf }

Q: How do I handle exceptions in ForEach-Object?

A: Use try-catch blocks inside the script block:

$items | ForEach-Object {
    try {
        Do-Something -Item $_
    } catch {
        Write-Error "Error processing $_: $_"
    }
}

Q: Can ForEach-Object replace Where-Object?

A: No, they serve different purposes. Use Where-Object for filtering, ForEach-Object for processing. Often combined: items | Where-Object {...} | ForEach-Object {...}

Q: Is ForEach-Object case-sensitive?

A: Variable names are case-insensitive ($_, $PSItem), but property/method names depend on the object.


Real-World Use Cases {#real-world-use-cases}

Use Case 1: Batch File Operations

# Copy and rename multiple files
Get-ChildItem -Path "C:\Source" -Filter "*.log" | ForEach-Object {
    $newName = "$(Get-Date -Format 'yyyyMM')-$($_.Name)"
    Copy-Item -Path $_.FullPath -Destination "C:\Archive\$newName"
    Write-Output "Archived: $newName"
}

Use Case 2: Active Directory User Management

# Disable inactive users
$inactiveUsers = Get-ADUser -Filter { lastLogonDate -lt (Get-Date).AddDays(-90) }

$inactiveUsers | ForEach-Object {
    Disable-ADAccount -Identity $_.ObjectGUID
    Write-Output "Disabled: $($_.SamAccountName)"
}

Use Case 3: System Health Check

# Check multiple servers for disk space
$servers = @("Server1", "Server2", "Server3")

$servers | ForEach-Object -Parallel -ThrottleLimit 3 {
    $disk = Get-PSDrive -Name "C" -ComputerName $_ -ErrorAction SilentlyContinue
    if ($disk) {
        $percentUsed = ($disk.Used / $disk.Total) * 100
        Write-Output "$_: $([Math]::Round($percentUsed, 2))% used"
    }
}

Use Case 4: Log File Processing

# Parse and summarize log files
Get-Content -Path "C:\Logs\*.txt" | ForEach-Object {
    if ($_ -match "ERROR") {
        [PSCustomObject]@{
            Timestamp = Get-Date
            Level = "ERROR"
            Message = $_
        }
    }
} | Group-Object -Property Level | Select-Object -Property Name, Count

Use Case 5: CSV Data Transformation

# Read CSV and add calculated column
Import-Csv -Path "C:\data.csv" | ForEach-Object {
    $_ | Add-Member -NotePropertyName "Quarter" -NotePropertyValue ([Math]::Ceiling([Int]$_.Month / 3))
    $_
} | Export-Csv -Path "C:\data-updated.csv" -NoTypeInformation

Conclusion

ForEach-Object is the most powerful and flexible way to iterate through collections in PowerShell. Its pipeline-aware design makes it ideal for processing cmdlet output, and its support for -Begin and -End blocks provides control for complex scenarios. With parallel processing in PowerShell 7+, ForEach-Object becomes even more powerful for handling large workloads efficiently.

Key Takeaways:

  • Use ForEach-Object for pipeline-friendly iteration
  • Combine with Where-Object for filtering
  • Use -Begin and -End blocks for setup and cleanup
  • Leverage parallel processing for I/O-bound tasks
  • Prefer foreach loop for complex multi-line logic
  • Always handle errors with try-catch

For more advanced PowerShell concepts, explore the Complete PowerShell Guide and related tutorials on Where-Object, Select-Object, and if-else statements.


Core Iteration & Looping

  • PowerShell For Loops - Counter-based iteration alternatives
  • PowerShell Foreach Loop - Direct collection iteration
  • PowerShell While Loops - Conditional iteration patterns

Filtering & Selection

Data Processing & Transformation

Data Structures

Control Flow & Logic

File Operations

Functions & Methods

Data Export & Conversion

Active Directory Operations

System Administration

Performance & Advanced

  • PowerShell Performance Tuning - Optimize loop performance
  • PowerShell Benchmarking - Performance testing with loops
  • PowerShell Parallelism - Parallel ForEach operations

Comprehensive Guides

  • Complete PowerShell Guide - Full PowerShell with iteration section
  • Complete PowerShell Tutorial - Comprehensive course
  • PowerShell Tutorial Complete - Full tutorial