Search code examples
powershellbreakpipeline

How to stop pipeline filtering (Where-Object) on first match


I searched but didn't find how to do it yet.
I am working on filtering data from large files (~2GB).
I used Where-Object and when it find match it continues to search for other matches which it makes sense.

Is it possible to stop it on the first match ?

For example (#1):

Get-Process | Where-Object {$_.ProcessName.StartsWith("svchost")}

The output will be:

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    666      38    26928      18672    92             568 svchost
    596      28    11516      16560    92             792 svchost
    425      14     5364       7036    45             832 svchost
    406      17     7032       8416    39            1004 svchost

What I want is to return the output after the first match:

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    666      38    26928      18672    92             568 svchost

This is what I tried (also with Foreach-Object):

Get-Process | Where-Object {if($_.ProcessName.StartsWith("svchost")){return $_}}
Get-Process | Where-Object {if($_.ProcessName.StartsWith("svchost")){return $_;break;}}    
Get-Process | ForEach-Object {if($_.ProcessName.StartsWith("svchost")){return $_}}

But it still returns the full output.
Reference:
How to break Foreach loop in Powershell?
Is it possible to terminate or stop a PowerShell pipeline from within a filter

EDIT (explanation about the problem with large data):
Example (#2):
I have two XMLs:
A.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Events>
  <Event>
    <EventData Name="Time">09/10/2017 12:54:16</EventData>
    <EventData Name="WorkstationName">USER2-PC</EventData>
    <EventData Name="UserName">user2</EventData>
  </Event>  
</Events>

B.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Events>
   <Event>
    <EventData Name="Time">09/10/2017 14:54:16</EventData>
    <EventData Name="WorkstationName">USER1-PC</EventData>
    <EventData Name="UserName">user1</EventData>
  </Event>
  <Event>
    <EventData Name="Time">09/10/2017 13:54:16</EventData>
    <EventData Name="WorkstationName">USER2-PC</EventData>
    <EventData Name="UserName">user2</EventData>
  </Event> 
 ... (more 100,000 events like the above two)
</Events>

These XMLs are being loaded as objects:

$fileA = "C:\tmp\A.xml"
$a = New-Object Xml.XmlDocument
$a.Load($fileA)

$fileB = "C:\tmp\B.xml"
$b = New-Object Xml.XmlDocument
$b.Load($fileB)

Then I want to search for the first match of the same username:

$result = $b.Events.Event | Where-Object {
    (($_.EventData | where-object {$_.Name -eq "UserName"})."#text" -eq $username)
}

$result.EventData

In this case it waste of time to run over the rest of 99,999 events if I have match on the first event.

EDIT (SOLVED):
After reading Nick answer, there wasn't anything new I didn't try.
The command:

Get-Process | Where-Object {if($_.ProcessName.StartsWith("svchost")){ $_;break;}}  

Indeed stops the Where-Object but it doesn't return the item.
This one can be solved by:

Get-Process | Where-Object {if($_.ProcessName.StartsWith("svchost")){ $someVar = $_;break;}}  

Therefore I marked his answer.


Solution

  • If efficiency is what you need you can try break it out in to a loop:

    Get-Process | foreach {If ($_.ProcessName.StartsWith("svchost")){$_;break}}
    

    You can confirm it works with this check:

    $i=0; Get-Process | foreach {$i++;$i; If ($_.ProcessName.StartsWith("svchost")){$_;break}}
    

    It will make the loop print out a number each time it loops, in my case it got to 115, Then if i do (Get-Process).Count I have 157 Processes, So it looped over my processes found the one we want and then stopped the loop.

    As stated here in other answers, You can use [0], On any array or list you can select a individual row using the index inside square brackets, Be careful though because attempting this on a null or empty object will throw a exception:

    (Get-Process | Where-Object {$_.ProcessName.StartsWith("svchost")})[0]
    

    Or you can you Select-Object which works in a similar way but has more options than just Index and will not throw any error if the object is null or empty.

    Get-Process | Where-Object {$_.ProcessName.StartsWith("svchost")} | Select-Object -First 1
    

    How ever both of these options will still evaluate the entire list before you select the first result.