PowerShell ForEach-Object: Complete Guide with Examples [2024]
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
- What is ForEach-Object?
- Why Use ForEach-Object?
- ForEach-Object Syntax
- Basic Pipeline Iteration
- Script Block Variables
- Processing Script Block
- Begin and End Script Blocks
- Processing Collections
- Working with Objects
- Advanced: Parallel Processing
- Performance Comparison
- Common Mistakes
- Best Practices
- Troubleshooting
- FAQs
- Real-World Use Cases
- 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:
- Pipeline Processing: Working with cmdlet output that streams one item at a time
- Object Transformation: Modifying or extracting properties from objects
- Side Effects: Executing commands for each item (moving files, updating records)
- Filtering and Mapping: Combined with Where-Object for data manipulation
- Parallel Processing: Processing large collections in parallel (PowerShell 7+)
- Large Collections: Memory efficient when working with millions of items
Comparison with Other Loop Methods:
| Feature | ForEach-Object | foreach Loop | for Loop |
|---|---|---|---|
| Pipeline-aware | ✅ Yes | ❌ No | ❌ No |
| Memory usage | Most efficient | Moderate | Moderate |
| One-liner | ✅ Easy | Harder | Harder |
| Parallel support | ✅ Yes (PS7+) | ❌ No | ❌ No |
| Access properties | Via $_ | Via variable | Via index |
| Best for | Cmdlet output | Complete collections | Counter loops |
| Readability | High | High | Moderate |
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-Memberor 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:variableNamefor 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.
Related Articles
Core Iteration & Looping
- PowerShell For Loops - Counter-based iteration alternatives
- PowerShell Foreach Loop - Direct collection iteration
- PowerShell While Loops - Conditional iteration patterns
Filtering & Selection
- PowerShell Where-Object - Filter collections before processing
- PowerShell Select-Object - Select properties and transform
- PowerShell Get-ChildItem Filter - Pre-filter file operations
Data Processing & Transformation
- PowerShell Arrays - Work with array collections
- PowerShell Strings - String manipulation in iterations
- PowerShell Replace Strings - Transform string data
- PowerShell Add-Member - Add properties to objects
Data Structures
- PowerShell Hashtables - Iterate hashtable key-value pairs
- PowerShell Variables - Variable management in loops
- PowerShell Output Table - Format iterated output
Control Flow & Logic
- PowerShell If-Else Statement - Conditional processing in ForEach
- PowerShell Switch Statement - Switch-based processing
- PowerShell Try-Catch - Error handling during iteration
File Operations
- PowerShell Get-Content - Process file content line by line
- PowerShell Output to File - Write iterated results
- PowerShell Delete Files - Batch file deletion
- PowerShell Rename Files - Batch rename operations
- PowerShell List Files - Process file listings
- PowerShell Copy-Item - Batch copy operations
Functions & Methods
- PowerShell Functions - Create reusable iteration functions
- PowerShell Get-Process - Process enumeration with ForEach
- PowerShell Get-Service - Service management with iteration
Data Export & Conversion
- PowerShell Export CSV - Export iterated data to CSV
- PowerShell Import CSV - Process CSV row by row
- PowerShell Format Table - Format processed output
Active Directory Operations
- PowerShell Active Directory Guide - Iterate AD objects
- PowerShell AD Security Guide - Process AD security items
- PowerShell Get-ADUser - Process AD users in batches
- DSACLS Permission Management - Process permissions
System Administration
- PowerShell Get CPU Usage - Monitor processes
- PowerShell Get Memory Usage - Process memory monitoring
- PowerShell Get File Properties - Extract file metadata
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