Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
February 14, 2025

ForEach vs. ForEach

I know I cover some advanced topics in this newsletter, which I know for some of you can be a bit overwhelming. I'll admit that I am often uncertain about what topics to cover. I've been using PowerShell for so long that it is difficult to remember what it was like to be a beginner, or at least someone with less experience. Sometimes I'll think a topic is too basic, but then I have to remember there's really no such thing as too basic. We all have to start somewhere.

Today, I want to explore the concept of ForEach in PowerShell. There is a ForEach-Object cmdlet and a ForEach statement. Unfortunately, the cmdlet has an alias of ForEach, which can be confusing. What are the differences? When should you use one over the other? Why does any of this matter? Let's dive in.

Pipeline First

One of PowerShell's most powerful features is the pipeline. The pipeline allows you to pass objects from one command to another.

PS C:\> Get-Service "bits","winrm","w32time","wsearch" | Restart-Service -Force -PassThru

Status   Name               DisplayName
------   ----               -----------
Running  bits               Background Intelligent Transfer Servi...
Running  w32time            Windows Time
Running  winrm              Windows Remote Management (WS-Managem...
Running  wsearch            Windows Search

You don't have manage each service individually. You get the service object with one command and hand it off to another command to do something with it. Before PowerShell, you would have to process each service individually.

dim service, serviceNames
'create an array variable for the service names
serviceNames = Array("bits","winrm","w32time","wsearch")
wscript.echo "Restarting services..."

for each n in serviceNames
    query = "select * from Win32_Service where Name='"+n+"'"
    set service = GetObject("winmgmts:").ExecQuery(query)
    for each s in service
        s.StopService()
        s.StartService()
        wscript.echo s.name
    next
next

wscript.echo "Services restarted."

In this VBScript example, I have to process each service name in a for each loop and then do the same with the WMI results. I'm very happy that I can replace 13 lines of VBScript that have to be saved to a file to a single line of PowerShell that I can run interactively.

However, there are times when you need to process each item in a collection individually. If I want to take the numbers 1 through 5 and multiply each one by two, I have to process each number separately. This is where ForEach comes in. Or I want to process a collection of names and display each on in all uppercase. This is where PowerShell's enumeration tools come in.

Foreach-Object

The story I heard back in the early days of PowerShell was that Microsoft recognize they needed an enumeration tool so someone wrote the ForEach-Object cmdlet. Then someone else wrote the ForEach statement for scripts, not realizing the overlap. I doubt this is 100% true, and as it turns out, both tools have their place.

The ForEach-Object cmdlet is designed to work with the pipeline. You can pipe objects to ForEach-Object and process each object individually. You can use the $_ automatic variable to reference the current object in the pipeline.

PS C:\> 1..5 | ForEach-Object {$_ * 2}
2
4
6
8
10

The code in the script block {} is executed once for each object in the pipeline. The $_ variable is a placeholder for the current object. If you find that confusing, you can use $PSItem.

1..5 | ForEach-Object {$PSItem * 2}

There is no limit to the code you can run in the script block. Typically, we keep things short when working in the console. But that is not a requirement.

dir c:\work\ -file | Group-Object Extension |
Where Count -ge 5 | Foreach-Object {
    $Name = $_.Name.Replace('.','')
    $Zip = Join-Path -Path d:\temp -ChildPath "$Name.zip"
    Write-Host "Archiving $($_.Count) $($_.Name) files to $zip" -ForegroundColor green
    $_.Group | Compress-Archive -DestinationPath $zip -CompressionLevel Optimal -Update
    Get-Item $Zip
}

I want to create a zip file for each file extension that has five or more files. I group the files by extension and then filter the groups. I then create a zip file for each group and update the zip file if it already exists. I then return the zip file object. There's no command to do this for me in a seamless pipeline like my script example. I have to process each GroupInfo object individually using the ForEach-Object cmdlet.

Compressing File Groups Individually
figure 1

There is more to ForEach-Object than this simple example. The cmdlet is a way that you can create an ad-hoc PowerShell function that processes pipeline input. Let me explain.

I'm sure many of you know that to create an advanced PowerShell function that accepts pipeline input, you need use three separate and named script blocks.

Function ZipFile {
    [CmdletBinding]
    Param($object)
    Begin {}
    Process {}
    End {}
}

The code in the Begin block runs once before the pipeline starts. The code in the Process block runs once for each object in the pipeline. The code in the End block runs once after the pipeline completes. The ForEach-Object cmdlet has similar blocks exposed through parameters.

ForEach-Object script blocks
figure 2

I'm only focusing on the relevant parameters.

Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
This email brought to you by Buttondown, the easiest way to start and grow your newsletter.