10

In PowerShell v2, the following line:

1..3| foreach { Write-Host "Value : $_"; $_ }| select -First 1

Would display:

Value : 1
1
Value : 2
Value : 3

Since all elements were pushed down the pipeline. However, in v3 the above line displays only:

Value : 1
1

The pipeline is stopped before 2 and 3 are sent to Foreach-Object (Note: the -Wait switch for Select-Object allows all elements to reach the foreach block).

How does Select-Object stop the pipeline, and can I now stop the pipeline from a foreach or from my own function?

Edit: I know I can wrap a pipeline in a do...while loop and continue out of the pipeline. I have also found that in v3 I can do something like this (it doesn't work in v2):

function Start-Enumerate ($array) {
    do{ $array } while($false)  
}

Start-Enumerate (1..3)| foreach {if($_ -ge 2){break};$_}; 'V2 Will Not Get Here'

But Select-Object doesn't require either of these techniques so I was hoping that there was a way to stop the pipeline from a single point in the pipeline.

2
  • 1
    So you are looking for StopUpstreamCommandsException, but you can't use that since it is internal. Here is a MS connect suggestion for it: connect.microsoft.com/PowerShell/feedback/details/768650/… Commented Jan 5, 2014 at 0:14
  • 1
    Thanks, @LarsTruijens for pointing me to that; I voted it up. Commented Jan 6, 2014 at 14:19

3 Answers 3

3

Check this post on how you can cancel a pipeline:
http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx

In PowerShell 3.0 it's an engine improvement. From the CTP1 samples folder ('\Engines Demos\Misc\ConnectBugFixes.ps1'):

# Connect Bug 332685
# Select-Object optimization
# Submitted by Shay Levi
# Connect Suggestion 286219
# PSV2: Lazy pipeline - ability for cmdlets to say "NO MORE"
# Submitted by Karl Prosser

# Stop the pipeline once the objects have been selected
# Useful for commands that return a lot of objects, like dealing with the event log

# In PS 2.0, this took a long time even though we only wanted the first 10 events
Start-Process powershell.exe -Args '-Version 2 -NoExit -Command Get-WinEvent | Select-Object -First 10'

# In PS 3.0, the pipeline stops after retrieving the first 10 objects
Get-WinEvent | Select-Object -First 10
Sign up to request clarification or add additional context in comments.

2 Comments

As far as I can tell it throws a StopUpstreamCommandsException exception, which is very similar to what Tobias is doing in the post I mentioned.
But unlike the PipelineStoppedException, Select-Object does not prevent downstream commands from completing. I would like to be able to stop the pipeline without requiring the user to know that they have to wrap the pipeline in a do-while or try-catch, but I suppose a can't use StopUpstreamCommandsException since it's a private type.
3

After trying several methods, including throwing StopUpstreamCommandsException, ActionPreferenceStopException, and PipelineClosedException, calling $PSCmdlet.ThrowTerminatingError and $ExecutionContext.Host.Runspace.GetCurrentlyRunningPipeline().stopper.set_IsStopping($true) I finally found that just utilizing select-object was the only thing that didn't abort the whole script (versus just the pipeline). [Note that some of the items mentioned above require access to private members, which I accessed via reflection.]

# This looks like it should put a zero in the pipeline but on PS 3.0 it doesn't
function stop-pipeline {
  $sp = {select-object -f 1}.GetSteppablePipeline($MyInvocation.CommandOrigin)
  $sp.Begin($true)
  $x = $sp.Process(0) # this call doesn't return
  $sp.End()
}

New method follows based on comment from OP. Unfortunately this method is a lot more complicated and uses private members. Also I don't know how robust this - I just got the OP's example to work and stopped there. So FWIW:

# wh is alias for write-host
# sel is alias for select-object

# The following two use reflection to access private members:
#   invoke-method invokes private methods
#   select-properties is similar to select-object, but it gets private properties

# Get the system.management.automation assembly
$smaa=[appdomain]::currentdomain.getassemblies()|
         ? location -like "*system.management.automation*"

# Get the StopUpstreamCommandsException class
$upcet=$smaa.gettypes()| ? name -like "*upstream*"

filter x {
  [CmdletBinding()]
  param(
    [parameter(ValueFromPipeline=$true)]
    [object] $inputObject
  )
  process {
    if ($inputObject -ge 5) {
      # Create a StopUpstreamCommandsException
      $upce = [activator]::CreateInstance($upcet,@($pscmdlet))

      $PipelineProcessor=$pscmdlet.CommandRuntime|select-properties PipelineProcessor
      $commands = $PipelineProcessor|select-properties commands
      $commandProcessor= $commands[0]

      $null = $upce.RequestingCommandProcessor|select-properties *

      $upce.RequestingCommandProcessor.commandinfo =  
          $commandProcessor|select-properties commandinfo

      $upce.RequestingCommandProcessor.Commandruntime =  
          $commandProcessor|select-properties commandruntime

      $null = $PipelineProcessor|
          invoke-method recordfailure @($upce, $commandProcessor.command)

      1..($commands.count-1) | % {
        $commands[$_] | invoke-method DoComplete
      }

      wh throwing
      throw $upce
    }
    wh "< $inputObject >"

    $inputObject
  } # end process
  end {
    wh in x end
  }
} # end filter x

filter y {
  [CmdletBinding()]
  param(
    [parameter(ValueFromPipeline=$true)]
    [object] $inputObject
  )
  process {
    $inputObject
  }
  end {
    wh in y end
  }
}

1..5| x | y | measure -Sum

PowerShell code to retrieve PipelineProcessor value through reflection:

$t_cmdRun = $pscmdlet.CommandRuntime.gettype()
# Get pipelineprocessor value ($pipor)
$bindFlags = [Reflection.BindingFlags]"NonPublic,Instance"
$piporProp = $t_cmdRun.getproperty("PipelineProcessor", $bindFlags )
$pipor=$piporProp.GetValue($PSCmdlet.CommandRuntime,$null)

Powershell code to invoke method through reflection:

$proc = (gps)[12] # semi-random process
$methinfo = $proc.gettype().getmethod("GetComIUnknown", $bindFlags)
# Return ComIUnknown as an IntPtr
$comIUnknown = $methinfo.Invoke($proc, @($true))

3 Comments

This doesn't do what I am looking for. 1..5| select -First 3 | measure -Sum returns a result, but 1..5| %{if($_ -ge 4) {stop-pipeline}} | measure -Sum does not. I want to stop new items from being sent through the pipeline, but allow the pipeline to finish processing.
Can you include the code for the invoke-method and select-properties functions? The supplied code won't work without those functions.
I wrote the invoke-method and select-properties at work, so I'd have to go through a whole process to get permission to publish them. However I'll add some PS reflection code to the example that will get you most of the way there.
1

I know that throwing a PipelineStoppedException stops the pipeline. The following example will simulate what you see with Select -first 1 in v3.0, in v2.0:

filter Select-Improved($first) {
    begin{
        $count = 0
    }
    process{
        $_
        $count++
        if($count -ge $first){throw (new-object System.Management.Automation.PipelineStoppedException)}
    }
}

trap{continue}
1..3| foreach { Write-Host "Value : $_"; $_ }| Select-Improved -first 1
write-host "after"

2 Comments

You have a typo: $fist > $first. And and 'new' should be new-object.
The problem I have with throwing a PipelineStoppedException is that commands further down the pipeline do not finish processing. This works : 1..5| select -first 3| measure, but this doesn't: 1..5| Select-Improved -first 3| measure

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.