PowerShell: the mysterious -RemainingScripts parameter of ForEach-Object
Here are it's details. ValueFromRemainingArguments
is set to true so your guess is correct.
help ForEach-Object
-RemainingScripts <ScriptBlock[]>
Takes all script blocks that are not taken by the Process parameter.
This parameter is introduced in Windows PowerShell 3.0.
gcm ForEach-Object | select -exp parametersets
Parameter Name: RemainingScripts
ParameterType = System.Management.Automation.ScriptBlock[]
Position = -2147483648
IsMandatory = False
IsDynamic = False
HelpMessage =
ValueFromPipeline = False
ValueFromPipelineByPropertyName = False
ValueFromRemainingArguments = True
Aliases = {}
Attributes =
System.Management.Automation.ParameterAttribute
System.Management.Automation.AllowEmptyCollectionAttribute
System.Management.Automation.AllowNullAttribute
I did more research and now feel confident to answer the behavior of -RemainingScripts parameter when multiple ScriptBlocks are passed in.
If you run the following commands and inspect the result carefully, you will find the pattern. It's not quite straightforward, but still not hard to figure out.
1..5 | foreach { "process block" } { "remain block" }
1..5 | foreach { "remain block" } -Process { "process block" }
1..5 | foreach { "remain block" } -End { "end block" } -Process { "process block" } -Begin { "begin block" }
1..5 | foreach { "remain block 1" } -End { "end block" } -Process { "process block" } { "remain block 2" }
1..5 | foreach { "remain block 1" } { "remain block 2" } -Process { "process block" } -Begin { "begin block" }
1..5 | foreach { "remain block 1" } { "remain block 2" } -Process { "process block" } { "remain block 3" }
1..5 | foreach { "process block" } { "remain block 1" } { "remain block 2" } -Begin { "begin block" }
1..5 | foreach { "process block" } { "remain block 1" } { "remain block 2" } { "remain block 3" }
So what's the pattern here?
When there's single ScriptBlock passed in: easy, it just goes to -Process (the most common usage)
When exactly 2 ScriptBlocks are passed in, there are 3 possible combinations
- -Process & -Begin -> execute as specified
- -Process & -End -> execute as specified
- -Process & -RemainingScripts -> Process becomes Begin, while RemainingScripts becomes Process
If we run these 2 statements:
1..5 | foreach { "process block" } { "remain block" }
1..5 | foreach { "remain block" } -Process { "process block" }
# Both of them will return:
process block
remain block
remain block
remain block
remain block
remain block
As you will find out, this is just a special case of the following test case:
When more than 2 ScriptBlocks are passed in, follow this workflow:
- Bind all scriptblocks as specified (Begin,Process,End); remaining ScriptBlocks go to RemainingScripts
- Order all scripts as: Begin > Process > Remaining > End
Result of ordering is a collection of ScriptBlocks. Let's call this collection OrderedScriptBlocks
- If Begin/End are not bound, just ignore
(Internally) Re-bind parameters based on OrderedScriptBlocks
- OrderedScriptBlocks[0] becomes Begin
- OrderedScriptBlocks[1..-2] become Process
- OrderedScriptBlocks[-1] (the last one) becomes End
Let's take this example
1..5 | foreach { "remain block 1" } { "remain block 2" } -Process { "process block" } { "remain block 3" }
Order result is:
{ "process block" } # new Begin
{ "remain block 1" } # new Process
{ "remain block 2" } # new Process
{ "remain block 3" } # new End
Now the execution result is completely predictable:
process block
remain block 1
remain block 2
remain block 1
remain block 2
remain block 1
remain block 2
remain block 1
remain block 2
remain block 1
remain block 2
remain block 3
That's the secret behind -RemainingScripts and now we understand more internal behavior of ForEach-Object!
Still I have to admit there's no documentation to support my guess (not my fault!), but these test cases should be enough to explain the behavior I described.