ForEach-Object and its scriptblocks

ForEach-Object is used for performing specified actions on every element of an array.

This array can be specified as a value of the -InputObject parameter, but in the majority of cases it is received from the pipeline.

We can use the ForEach-Object cmdlet to process several elements in parallel, or to get the value of some property or call some method of the elements passed to the cmdlet.

But in this article we concentrate on its classic use case – executing several scriptblocks on the received objects.

To access the element being processed we use automatic variables $_ or $PSItem, which are equivalent.

Process

In the simplest case we use ForEach-Object cmdlet as follows.

Get-Service | ForEach-Object -Process { if ($_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped') { Start-Service $_ } }

Or we can use a variable.

$ScriptBlock = { if ($_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped') { Start-Service $_ } }
​
Get-Service | ForEach-Object -Process $ScriptBlock

In the examples we specified the scriptblock as a value of the -Process parameter, which is executed for every service object, passed by the pipeline.

Alias

The ForEach-Object cmdlet has two aliases – foreach and %. Thus, the following command are fully interchangeable.

Get-Service | ForEach-Object Name
Get-Service | foreach Name
Get-Service | % Name

However, back to our scriptblocks.

Begin and End

Except the -Process parameter we can specify the -Begin and -End parameters. Each of them is executed single time, despite the number of received objects: -Begin is invoked before processing the elements, and -End is executed after the processing is finished.

Both these parameters accept scriptblock, but they differ from the -Process parameter in two aspects: firstly, they accept a scalar value, but the -Process parameter accepts array of scriptblocks, and, secondly, the scriptblock specified as a value of any of these two parameters cannot access the elements by using the $_ or $PSItem automatic variables.

In essence, -Begin is used for preparing for the processing of elements, and -End is called for some finishing actions.

Get-Process | ForEach-Object -Begin { [long]$WorkingSet = 0 } -Process { $WorkingSet += $_.WS } -End { $WorkingSet }

In addition to -Begin, -Process, and -End, there is another parameter: -RemainingScripts. It, like the -Process parameter, accepts an array of scriptblocks, and, when the ForEach-Object cmdlet runs, they are added to the scriptblocks specified in the -Process parameter. Why it is needed we will talk later.

Positional parameters

Because of the PowerShell’s ability to use positional parameters, in many cmdlets we can skip parameter names and specify only their arguments.

So, we can specify a command as follows.

"Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"}

If we invoke the Trace-Command cmdlet for ParameterBinding component

Trace-Command -Name ParameterBinding -Expression { "Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} } -PSHost

we’ll see that the first argument – {“1: $_”} – is bound to the -Process parameter, and others are associated with -RemainingScripts.

This is because in the code of the -RemainingScripts parameter the ValueFromRemainingArguments attribute is specified as equal to true, and this means, that all the arguments not bound to other parameters is bound to this one.

If we execute the original command, we get the following result.

1:
2: Value
3:

Why is this?

As we talked earlier, when the ForEach-Object cmdlet runs, the arguments of the -RemainingScripts parameter are added to the common array of scriptblocks, after those, that was specified in the -Process parameter. After that, PowerShell checks whether the -Begin and -End parameters are specified. If not, then the first scriptblock in the array becomes the begin scriptblock, as if it was specified in the -Begin parameter, and the last scriptblock becomes the end scriptblock, which is similar to specifying it in the -End parameter.

If we pass the ForEach-Object cmdlet an array of two objects, we’ll see this more clearly.

"Value1", "Value2" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"}
1:
2: Value1
2: Value2
3:

It is interesting, that if we specify two scriptblocks, then the first one will be the begin scriptblock, and other will be executed for every object.

"Value1", "Value2" | ForEach-Object {"1: $_"} {"2: $_"}
1:
2: Value1
2: Value2

If we specify more than two scriptblocks, then, as we saw earlier, the first and the last become the begin and the end scriptblock, respectively.

"Value1", "Value2" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} {"4: $_"}
1:
2: Value1
3: Value1
2: Value2
3: Value2
4:

Begin and End again

In order to all the scriptblocks execute for the received objects, we can specify the -Begin and -End parameters in the command. For example, like this.

"Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} -Begin {} -End {}

Or like this.

"Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} -Begin $null -End $null

In either case, the result is as follows.

1: Value
2: Value
3: Value

Also, we can add empty scriptblocks as the first and the last arguments.

"Value" | ForEach-Object {} {"1: $_"} {"2: $_"} {"3: $_"} {}

Considering all we have discussed, we can define the command we used in the beginning as follows.

Get-Process | ForEach-Object { [long]$WorkingSet = 0 } { $WorkingSet += $_.WS } { $WorkingSet }

Return

The ForEach-Object cmdlet is very important element of PowerShell. It supports several different use models (as we said, we discussed only one of them), and two types of syntax. But it is its finesse and ability to hide its intrinsic complexity, providing in most cases quite simple solutions to different problems, that makes it incredibly easy to use.

2 thoughts on “ForEach-Object and its scriptblocks

  1. Rodion Esartiia June 14, 2020 / 4:09 pm

    That’s an awesome and quick guide of really important things in ForEach-Object.

    Thank you for sharing it!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s