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

    1. -Process & -Begin -> execute as specified
    2. -Process & -End -> execute as specified
    3. -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:

    1. Bind all scriptblocks as specified (Begin,Process,End); remaining ScriptBlocks go to RemainingScripts
    2. Order all scripts as: Begin > Process > Remaining > End
    3. Result of ordering is a collection of ScriptBlocks. Let's call this collection OrderedScriptBlocks

      • If Begin/End are not bound, just ignore
    4. (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.