Home PowerShell Internals Pipeline Why “Continue” Can Stop Upstream Cmdlets
 

Why “Continue” Can Stop Upstream Cmdlets

Do you know what the keywords "break" and "continue" do in a pipeline? I was not so sure, so I tested a bit and came to some surprising conclusions that may help you speed up your scripts.

Continue and Break in Classic Loops

In a classic "for", "foreach", and "while" statement, "continue" would skip a cycle, and "break" would leave the loop. So you can do things like this:

  

for ($i = 1; $i -lt 100; $i++)
{
    if ($i % 2) { continue }
    $i
}

  Outputting Even Numbers With For

Or this:

  

foreach($i in (1..100))
{
  if ($i % 2) { continue }
  $i
}

  Outputting Even Numbers with Foreach

Or this:

  

$i = 0
while ($i -le 100)
{
  $i++
  if ($i % 2) { continue }
  $i
}

  Outputting Even Numbers With While

Note: do not put the incrementor ($i++) below the line that calls continue, or else it will not be incremented anymore once you hit the condition, thus producing an endless loop – the intrinsic risk of while statements.

"Break" works almost the same. It would not skip a cycle but the entire remaining cycles.

Continue and Break in the Pipeline

In the PowerShell pipeline, this is a whole different ballgame. This example will fail:

  

1...100 | ForEach-Object {
  if ($_ % 2) { continue }
  $_
}

  "Continue" in a Pipeline aborts the Pipeline altogether

No output. Once there is a "continue" statement inside your pipeline, it will stop immediately.

And since foreach is just an anonymous function, a pipeline-aware function would act the same:

  

function test
{
  param([Parameter(ValueFromPipeline=$true)]$in)

  process
  {
    if ($in % 2) { continue }
    $in 
  }
}

1..100 | test

  Pipeline-aware Functions break on continue as well

You would have to rewrite the function and omit the use of "continue":

  

function test
{
  param([Parameter(ValueFromPipeline=$true)]$in)

  process
  {
    if ($in % 2)
    {
      # do nothing
    }
    else
    {
      $in
    } 
  }
}

1..100 | test

  Better not use "continue" inside the pipeline

Of course, these are just mind-teasers to prove the point. You could as well use Where-Object (which is coincidentally just a Foreach-Object with a built-in if-condition):

  

1..100 | Where-Object { -not ($_ % 2) }

  Outputting Even Numbers with Where-Object

When you *Should* use "Continue" in a Pipeline

As you have just seen, "continue" will abort a pipeline operation – and that can be very useful. Aborting a pipeline operation will tell the upstream cmdlets that you are done. So if a pipeline has already collected what you wanted, you can then use "continue" to stop the pipeline and walk away.

Here is a sample script that should get all exe files it finds anywhere inside your windows folder. As you will discover, it takes quite a while for this line to run:

  

Get-ChildItem -Path $env:windir -Filter *.exe -Recurse -ErrorAction SilentlyContinue

  Finding all EXE files in your Windows folder

If you just wanted to get the first 10 exe files, then what? The line gets you all exe files. With "continue", you can stop the pipeline:

  

Get-ChildItem -Path $env:windir -Filter *.exe -Recurse -ErrorAction SilentlyContinue |
  ForEach-Object -Begin { $counter = 0 } -Process {
   $counter ++
   if ($counter -gt 10) { continue }
   $_
  }

  Getting only the first 10 EXE files

Unfortunately, when you abort the pipeline with "continue", not only will it stop all upstream cmdlets, but also not return anything. It simply outputs the data to screen. So you cannot save it to a variable:

  

$result = Get-ChildItem -Path $env:windir -Filter *.exe -Recurse -ErrorAction SilentlyContinue |
  ForEach-Object -Begin { $counter = 0 } -Process {
   $counter ++
   if ($counter -gt 10) { continue }
   $_
  }

$result

  Data will be outputted directly, and $result is empty

Since "continue" was originally designed to stop a loop, to make this work you simply need to enclose it into a loop, and receive the results from that loop:

  

$result = for ($i = 1; $i -lt 2; $i++)
{
  Get-ChildItem -Path $env:windir -Filter *.exe -Recurse -ErrorAction SilentlyContinue |
  ForEach-Object -Begin { $counter = 0 } -Process {
    $counter ++
    if ($counter -gt 10) { continue }
    $_
  }
}
$result

  Using an Encapsulating Loop to receive pipeline results

Official Solution – Available Since PowerShell 3.0

Of couse, the previous hack was just that: a lot of cryptic code – but useful. Silently and widely unnoticed, the very same functionality has been implemented into PowerShell 3.0 and above. Now all is much easier.

To get only the first 10 EXE files in your windows folder and then stop all upstream cmdlets, all you need to do is this:

  

Get-ChildItem -Path $env:windir -Filter *.exe -Recurse -ErrorAction SilentlyContinue |
  Select-Object -First 10 -ExpandProperty FullName

  Getting Only First 10 EXE Path Names

Note: I added -ExpandProperty to also just output the file path. You can take that parameter out if you want the complete file objects back, of course.

So Select-Object has a parameter called -First that is much much more useful than most people think. Beginning in PowerShell 3.0, -First will also stop upstream cmdlets (which it did not in PowerShell 2.0, so in PowerShell 2.0 the above line would run forever).

What it means to YOU

If you know beforehand just how many results you expect, then always add "Select-Object -First x", and replace "x" with the number of results you expect.

This will guarantee that all upstream cmdlets stop once the results are in. Maybe they would have, anyway. Maybe not. With Select-Object -First x, you always know for sure.

Here's another last example, illustrating the point. Let's assume for a second you want the first 10 event log entries in your system eventlog that have an instance id between 10000 and 20000.

Since Get-EventLog has no advanced filters that you could use, you would have to filter yourself like this:

  

Get-EventLog -LogName System |
  Where-Object InstanceID -ge 10000 |
  Where-Object InstanceID -le 20000

  Getting InstanceIDs between 10.000 and 20.000

To get just the first 10, simply add Select-Object. Once the first 10 results are in, PowerShell stops immediately and will not retrieve all the remaining entries anymore:

  

Get-EventLog -LogName System |
  Where-Object InstanceID -ge 10000 |
  Where-Object InstanceID -le 20000 |
  Select-Object -First 10

  Getting Only 10 Results, then Stopping Upstream Cmdlets