Powershell: break in the pipeline: Why does End run in some cases and not in others?

Created on 7 Jan 2018  路  6Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

do { 
    1..10 | 
        % { if ($_ -gt 2) { break }; $_ } | 
        Sort-Object -Descending
 } while ($false)

do {
    % { foreach ( $i in 100..110 ) { $i } } | 
    % { if ($_ -gt 102) { break }; $_ } | 
    Sort-Object -Descending
} while ($false)

Expected behavior

Either no output or

2
1
0
102
101
100

Actual behavior

102
101
100

Environment data

> $PSVersionTable

Name                           Value                                            
----                           -----                                            
PSVersion                      6.0.0-rc.2                                       
PSEdition                      Core                                             
GitCommitId                    v6.0.0-rc.2                                      
OS                             Microsoft Windows 6.3.9600                       
Platform                       Win32NT                                          
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                          
PSRemotingProtocolVersion      2.3                                              
SerializationVersion           1.1.0.1                                          
WSManStackVersion              3.0                                              
Issue-Discussion Resolution-Answered WG-Engine

Most helpful comment

@alx9r - your understanding is correct.

Note that you could have used & instead of % in the first stage of the pipeline and gotten the same result - the key is to ensure the foreach loop statement has not completed before hitting the break statement. As @mklement0 points out, $() would fully execute the loop before writing anything to the pipeline.

All 6 comments

This arose while trying to reproduce the examples in #3821.

I don't have an explanation, but the issue is related to using % (ForEach-Object) at the _start_ of the pipeline.

If you replace the initial % {... } with $(...) - which is the more idiomatic way to start a pipeline with an expression - the problem goes away (yields no output, as expected, because Sort-Object's end block doesn't get to run):

do {
  $(foreach ( $i in 100..110 ) { $i }) |   # Use $(...) rather than % {...}
  % { if ($_ -gt 102) { break }; $_ } | 
  Sort-Object -Descending
} while ($false)

Still, it would be good to have an explanation for your symptom.

@izybkr wrote this in #3897 (comment):

break and continue work dynamically - meaning the break/continue statement searches for an appropriate loop to break from at runtime.
...
Under the covers, the break turns into an exception (always V2 or earlier, V3 onwards if not lexically within a loop statement), the exception is always silent (because you don't really want to think of break as an exception, so it's silent even if we don't find a matching loop.

By that reasoning, I _think_

do { 
    1..10 | 
        % { if ($_ -gt 2) { break }; $_ } | 
        Sort-Object -Descending
 } while ($false)

outputs nothing because the "exception" thrown by break is not caught until it reaches the do{} which is the first loop encountered. That loop is outside the pipeline, so flow of control continues outside the pipeline and end{} doesn't run. On the other hand I _think_ in

do {
    % { foreach ( $i in 100..110 ) { $i } } | 
    % { if ($_ -gt 102) { break }; $_ } | 
    Sort-Object -Descending
} while ($false)

the first loop encountered by the "exception" thrown by break is foreach{}, so flow of control continues at the closing } of the foreach and the pipeline continues as usual from there.

@lzybkr Is this right?

@alx9r - your understanding is correct.

Note that you could have used & instead of % in the first stage of the pipeline and gotten the same result - the key is to ensure the foreach loop statement has not completed before hitting the break statement. As @mklement0 points out, $() would fully execute the loop before writing anything to the pipeline.

This all makes sense to me now and seems to match the behavior I'm seeing. Thank you @lzybkr.

I hadn't even considered the execute-in-full-first aspect - good to know that & { ... } - as opposed to $(...) - can send an expression-based statement's output to the pipeline as it is being produced.

Was this page helpful?
0 / 5 - 0 ratings