PowerShell Pipeline iteration - powershell

I often write functions that need to accept pipelines like:
function Invoke-ExchangeHubChecks
{
[cmdletbinding()]
param
(
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[object[]]$oServersToTest,
[int]$QueueWarnThreshold = 50,
[int]$QueueFailThreshold = 100
)
begin
{
$oHubTests = #()
$c=0
}
Process
{
$c++
$Server = $oServersToTest[0]
$oHubTests += [pscustomobject]#{
Identity = $server.Identity
Queue = "blah"
Count = $c
}
}
end
{
$oHubTests
}
}
end
{
$oHubTests
}
}
I often see code snippets with a foreach inside the process block. My understanding is that the process block replaces the need for the foreach block (in most cases).
I need to add a "filter" to the input object "$oServersToTest". I have used "where-object" to create the filter:
$Server = $oServersToTest[0] | Where-Object {$_.IsHub}
However, as it iterates over the collection, it is adding objects that don't match the where clause to the custom object, proved by $c:
Output:
Identity Queue Count
-------- ----- -----
Server01 blah 1
Server02 blah 2
Server03 blah 3
Server04 blah 4
Server05 blah 5
blah 6
blah 7
Server06 blah 8
Server07 blah 9
Server08 blah 10
Server09 blah 11
Server10 blah 12
blah 13
blah 14
Is this an example where I must use a foreach inside the process block:
Process
{
$c++
#$Server = $oServersToTest[0] | Where-Object{$_.IsHubServer}
$TestResult = #()
$oServersToTest | Where-Object {$_.IsHubServer} | Foreach-object{
$TestResult += [pscustomobject]#{
Identity = $_.Identity
Queue = "blah"
Count = $c
}
}
$oHubTests += $TestResult
}
End
{
$oHubTests
}
Which works, but why does this work and not the other way? To me, it's doing twice the work.
(Powershell 5.0)
TIA

I would use a foreach() loop inside the process block.
As Jeff Zeitlin points out, this also lets you support processing of collections with named parameters and pipelining alike.
The reason for using foreach() over ForEach-Object is three-fold:
Performance:
When the input collection is already in memory (which it is in both cases), the overhead from implicit parameter binding will make ForEach-Object slower than foreach()
Readability:
IMO, having a named variable (like $Server) makes the script more readable and you avoid confusion with nested ForEach-Object or Where-Object clauses inside the loop
Functionality:
With a plain ol' foreach(), you get fast flow control options like break and continue for free.
process {
$oHubTests += foreach($server in $oServersToTest |Where-Object {$_.IsHubServer}){
$c++
[pscustomobject]#{
Identity = $server.Identity
Queue = "blah"
Count = $c
}
}
}
If the only thing your cmdlet does is take the input and construct corresponding [pscustomobject]'s, then I would cut out the $oHubTests part completely, and just output the objects as they flow through:
function Invoke-ExchangeHubChecks
{
[CmdletBinding()]
param
(
[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[object[]]$oServersToTest,
[int]$QueueWarnThreshold = 50,
[int]$QueueFailThreshold = 100
)
begin {
$c=0
}
process {
foreach($server in $oServersToTest |Where-Object {$_.IsHubServer}){
$c++
[pscustomobject]#{
Identity = $server.Identity
Queue = "blah"
Count = $c
}
}
}
}

You are addressing one of the most complicated design structures of powershell for me. Creating a cmdlet that can process an array of objects as provided as a parameter or per item piped into the cmdlet.
For example do-something -input $items or $items | do-something.
The process block is meant mostly for when a cmdlet accept pipeline values. It doesn't replace the for loop as you think it. The pipe has already done an implicit loop on the collection although that is not 100% accurate but that is a different story. I'll continue with this somehow inaccurate statement for simplicity.
With such cases, the begin,process and end allow you to control what happens at each part of the pipe or imaginary loop. Before and end are executed only once and can used for initialization and finalizing. In the above example first begin executed, then process for each item piped from the collection and then end. But if the collection is used as a normal parameter then it's begin, process with a loop inside and then end.
I understand that is complicated but try to visualize with a do-something implementation that just writes to the host what is going on.
I had to understand this sequence when developing MarkdownPS. Check this example and the examples of usage on the root. Get-ChildItem | Select-Object Name | New-MDList. FYI the last example is a demonstration of the inaccurate statement I used in this answer.

Related

Why should I avoid using the increase assignment operator (+=) to create a collection

The increase assignment operator (+=) is often used in [PowerShell] questions and answers at the StackOverflow site to construct a collection objects, e.g.:
$Collection = #()
1..$Size | ForEach-Object {
$Collection += [PSCustomObject]#{Index = $_; Name = "Name$_"}
}
Yet it appears an very inefficient operation.
Is it Ok to generally state that the increase assignment operator (+=) should be avoided for building an object collection in PowerShell?
Yes, the increase assignment operator (+=) should be avoided for building an object collection, see also: PowerShell scripting performance considerations.
Apart from the fact that using the += operator usually requires more statements (because of the array initialization = #()) and it encourages to store the whole collection in memory rather then push it intermediately into the pipeline, it is inefficient.
The reason it is inefficient is because every time you use the += operator, it will just do:
$Collection = $Collection + $NewObject
Because arrays are immutable in terms of element count, the whole collection will be recreated with every iteration.
The correct PowerShell syntax is:
$Collection = 1..$Size | ForEach-Object {
[PSCustomObject]#{Index = $_; Name = "Name$_"}
}
Note: as with other cmdlets; if there is just one item (iteration), the output will be a scalar and not an array, to force it to an array, you might either us the [Array] type: [Array]$Collection = 1..$Size | ForEach-Object { ... } or use the Array subexpression operator #( ): $Collection = #(1..$Size | ForEach-Object { ... })
Where it is recommended to not even store the results in a variable ($a = ...) but immediately pass it into the pipeline to save memory, e.g.:
1..$Size | ForEach-Object {
[PSCustomObject]#{Index = $_; Name = "Name$_"}
} | ConvertTo-Csv .\Outfile.csv
Note: Using the System.Collections.ArrayList class could also be considered, this is generally almost as fast as the PowerShell pipeline but the disadvantage is that it consumes a lot more memory than (properly) using the PowerShell pipeline.
see also: Fastest Way to get a uniquely index item from the property of an array and Array causing 'system.outofmemoryexception'
Performance measurement
To show the relation with the collection size and the decrease of performance you might check the following test results:
1..20 | ForEach-Object {
$size = 1000 * $_
$Performance = #{Size = $Size}
$Performance.Pipeline = (Measure-Command {
$Collection = 1..$Size | ForEach-Object {
[PSCustomObject]#{Index = $_; Name = "Name$_"}
}
}).Ticks
$Performance.Increase = (Measure-Command {
$Collection = #()
1..$Size | ForEach-Object {
$Collection += [PSCustomObject]#{Index = $_; Name = "Name$_"}
}
}).Ticks
[pscustomobject]$Performance
} | Format-Table *,#{n='Factor'; e={$_.Increase / $_.Pipeline}; f='0.00'} -AutoSize
Size Increase Pipeline Factor
---- -------- -------- ------
1000 1554066 780590 1.99
2000 4673757 1084784 4.31
3000 10419550 1381980 7.54
4000 14475594 1904888 7.60
5000 23334748 2752994 8.48
6000 39117141 4202091 9.31
7000 52893014 3683966 14.36
8000 64109493 6253385 10.25
9000 88694413 4604167 19.26
10000 104747469 5158362 20.31
11000 126997771 6232390 20.38
12000 148529243 6317454 23.51
13000 190501251 6929375 27.49
14000 209396947 9121921 22.96
15000 244751222 8598125 28.47
16000 286846454 8936873 32.10
17000 323833173 9278078 34.90
18000 376521440 12602889 29.88
19000 422228695 16610650 25.42
20000 475496288 11516165 41.29
Meaning that with a collection size of 20,000 objects using the += operator is about 40x slower than using the PowerShell pipeline for this.
Steps to correct a script
Apparently some people struggle with correcting a script that already uses the increase assignment operator (+=). Therefore, I have created a little instruction to do so:
Remove all the <variable> += assignments from the concerned iteration, just leave only the object item. By not assigning the object, the object will simply be put on the pipeline.
It doesn't matter if there are multiple increase assignments in the iteration or if there are embedded iterations or function, the end result will be the same.
Meaning, this:
 
ForEach ( ... ) {
$Array += $Object1
$Array += $Object2
ForEach ( ... ) {
$Array += $Object3
$Array += Get-Object
}
}
Is essentially the same as:
ForEach ( ... ) {
$Object1
$Object2
ForEach ( ... ) {
$Object3
Get-Object
}
}
Note: if there is no iteration, there is probably no reason to change your script as likely only concerns a few additions
Assign the output of the iteration (everything that is put on the pipeline) to the concerned a variable. This is usually at the same level as where the array was initialized ($Array = #()). e.g.:
 
$Array = ForEach ( ... ) { ...
Note 1: Again, if you want single object to act as an array, you probably want to use the Array subexpression operator #( ) but you might also consider to do this at the moment you use the array, like: #($Array).Count or ForEach ($Item in #($Array))
Note 2: Again, you're better off not assigning the output at all. Instead, pass the pipeline output directly to the next cmdlet to free up memory: ... | ForEach-Object {...} | Export-Csv .\File.csv.
Remove the array initialization <Variable> = #()
For a full example, see: Comparing Arrays within Powershell
Note that the same applies for using += to build strings (
see: Is there a string concatenation shortcut in PowerShell?) and also building HashTables like:
$HashTable += #{ $NewName = $Value }

Select-Object has different behavior using piping vs. -InputObject

can someone shed light on why example 2 does not have the same result as example 1? I would think that $a and $b2 should be the same. $b2 is null! I am writing a script where using the method in example 2 is preferable.
example 1:
$a = Get-Content $some_text_file | Select-Object -Skip 1
example 2:
$b1 = Get-Content $some_text_file
$b2 = Select-Object -InputObject $b1 -Skip 1
edit: using this syntax gets me where I need to be.
$b1 = Get-Content $file
$b2 = $b1 | Select-Object -Skip 1
As Lee_Dailey notes, this is expected behavior
can someone shed light on why
This has to do with how cmdlets execute in the pipeline.
As you might know, the core functionality of a cmdlet is made up of three methods:
BeginProcessing()
ProcessRecord()
EndProcessing()
*(the begin/process/end blocks in an advanced function correspond to these).
BeginProcessing() and EndProcessing() are always executed exactly once. How many times ProcessRecord() execute depend on whether it's the first command in a pipeline or not.
When a cmdlet occurs as the first element in a pipeline (ie. there are no | sign to the left of it), ProcessRecord() executes once.
When a cmdlet receives input from an upstream command in its pipeline, however, ProcessRecord() is run once for each input item coming in through the pipeline.
With this in mind, please consider this simplified version of Select-Object:
function Select-FakeObject {
param(
[Parameter(Mandatory, ValueFromPipeline)]
[object[]]$InputObject,
[Parameter()]
[int]$Skip = 0
)
begin {
$skipped = 0
}
process {
if($skipped -lt $Skip){
$skipped++
Write-Host "Skipping $InputObject"
}
else{
Write-Host "Selecting $InputObject"
Write-Output $InputObject
}
}
}
Now, let's try both scenarios with this dummy function:
PS C:\> $a = 1,2,3
PS C:\> $b1 = $a |Select-FakeObject -Skip 1
We'll see that PowerShell indeed calls the process block once per input item:
Skipping 1
Selecting 2
Selecting 3
whereas if we pass the object like in your second example:
PS C:\> $a = 1,2,3
PS C:\> $b2 = Select-FakeObject -Skip 1
Skipping 1 2 3
We now see that the process block only executes once, over all of $a rather than the individual items.

Pipe a single object and process it without For-EachObject

Original Question
I a piping a single string and processing it with For-EachObject as follows:
"Test" | % { $_.Substring(0,1) }
It seems wrong to process a single piped item using For-EachObject, partly because it's misleading to future code maintainers. I don't know any other way, though, to capture the string while saying "it's just a single item." For instance, this doesn't work.
"Test" | $_.Substring(0,1)
"Test" | { $_.Substring(0,1) }
How can I process a single object while indicating that I expect only one?
Edit: Add the actual use case
The above is a simplified version of what I'm actually trying to accomplish. I am getting the first paragraph of a Wikipedia article, which is part of a larger function that saves the result to a file.
curl "www.wikipedia.org/wiki/Hope,_British_Columbia" |
select -expand allelements |
? { $_.id -eq "mw-content-text" } |
select -expand innerHTML |
% {
$i = $_.IndexOf("<P>");
$j = $_.IndexOf("</P>");
$_.Substring($i, $j - $i) -replace '<[^>]*>'
}
The part that needs to process a single object follows the select -expand innerHtml expression. Piping is my preferred way because putting multiple parenthesis around the curl part seems ugly.
Aliases
curl is Invoke-WebRequest
select is Select-Object
-expand is ExplandProperty
? is Where-Object
% is For-EachObject
If you are creating single-purpose code where you control both the input and the output, and there will always be only one object, then using the pipeline is overkill and not really appropriate. Just pass the string as a parameter to your function.
function f([String]$s) {
$s.Substring(0,1)
}
PS> f "Test"
T
If you're building a general-purpose function to take input from the pipeline, your function needs to account for more than one object in the stream. Fortunately PowerShell has a natural way to do this using the Process{} block, which is executed once for each item in the input pipeline.
function f {
param(
[Parameter(ValueFromPipeline=$true)]
[String]$item
)
process {
$item.Substring(0,1)
}
}
PS> '123','abc','#%#' | f
1
a
#
This is a common enough function that PowerShell has a shorthand for writing a function that takes one parameter from the pipeline and only contains a process block.
filter f {
$_.SubString(0,1)
}
PS> '123','abc','#%#' | f
1
a
#

How does Select-Object stop the pipeline in PowerShell v3?

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.
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
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))
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"

Is it possible to terminate or stop a PowerShell pipeline from within a filter

I have written a simple PowerShell filter that pushes the current object down the pipeline if its date is between the specified begin and end date. The objects coming down the pipeline are always in ascending date order so as soon as the date exceeds the specified end date I know my work is done and I would like to let tell the pipeline that the upstream commands can abandon their work so that the pipeline can finish its work. I am reading some very large log files and I will frequently want to examine just a portion of the log. I am pretty sure this is not possible but I wanted to ask to be sure.
It is possible to break a pipeline with anything that would otherwise break an outside loop or halt script execution altogether (like throwing an exception). The solution then is to wrap the pipeline in a loop that you can break if you need to stop the pipeline. For example, the below code will return the first item from the pipeline and then break the pipeline by breaking the outside do-while loop:
do {
Get-ChildItem|% { $_;break }
} while ($false)
This functionality can be wrapped into a function like this, where the last line accomplishes the same thing as above:
function Breakable-Pipeline([ScriptBlock]$ScriptBlock) {
do {
. $ScriptBlock
} while ($false)
}
Breakable-Pipeline { Get-ChildItem|% { $_;break } }
It is not possible to stop an upstream command from a downstream command.. it will continue to filter out objects that do not match your criteria, but the first command will process everything it was set to process.
The workaround will be to do more filtering on the upstream cmdlet or function/filter. Working with log files makes it a bit more comoplicated, but perhaps using Select-String and a regular expression to filter out the undesired dates might work for you.
Unless you know how many lines you want to take and from where, the whole file will be read to check for the pattern.
You can throw an exception when ending the pipeline.
gc demo.txt -ReadCount 1 | %{$num=0}{$num++; if($num -eq 5){throw "terminated pipeline!"}else{write-host $_}}
or
Look at this post about how to terminate a pipeline: https://web.archive.org/web/20160829015320/http://powershell.com/cs/blogs/tobias/archive/2010/01/01/cancelling-a-pipeline.aspx
Not sure about your exact needs, but it may be worth your time to look at Log Parser to see if you can't use a query to filter the data before it even hits the pipe.
If you're willing to use non-public members here is a way to stop the pipeline. It mimics what select-object does. invoke-method (alias im) is a function to invoke non-public methods. select-property (alias selp) is a function to select (similar to select-object) non-public properties - however it automatically acts like -ExpandProperty if only one matching property is found. (I wrote select-property and invoke-method at work, so can't share the source code of those).
# Get the system.management.automation assembly
$script:smaa=[appdomain]::currentdomain.getassemblies()|
? location -like "*system.management.automation*"
# Get the StopUpstreamCommandsException class
$script:upcet=$smaa.gettypes()| ? name -like "*StopUpstreamCommandsException *"
function stop-pipeline {
# Create a StopUpstreamCommandsException
$upce = [activator]::CreateInstance($upcet,#($pscmdlet))
$PipelineProcessor=$pscmdlet.CommandRuntime|select-property PipelineProcessor
$commands = $PipelineProcessor|select-property commands
$commandProcessor= $commands[0]
$ci = $commandProcessor|select-property commandinfo
$upce.RequestingCommandProcessor | im set_commandinfo #($ci)
$cr = $commandProcessor|select-property commandruntime
$upce.RequestingCommandProcessor| im set_commandruntime #($cr)
$null = $PipelineProcessor|
invoke-method recordfailure #($upce, $commandProcessor.command)
if ($commands.count -gt 1) {
$doCompletes = #()
1..($commands.count-1) | % {
write-debug "Stop-pipeline: added DoComplete for $($commands[$_])"
$doCompletes += $commands[$_] | invoke-method DoComplete -returnClosure
}
foreach ($DoComplete in $doCompletes) {
$null = & $DoComplete
}
}
throw $upce
}
EDIT: per mklement0's comment:
Here is a link to the Nivot ink blog on a script on the "poke" module which similarly gives access to non-public members.
As far as additional comments, I don't have meaningful ones at this point. This code just mimics what a decompilation of select-object reveals. The original MS comments (if any) are of course not in the decompilation. Frankly I don't know the purpose of the various types the function uses. Getting that level of understanding would likely require a considerable amount of effort.
My suggestion: get Oisin's poke module. Tweak the code to use that module. And then try it out. If you like the way it works, then use it and don't worry how it works (that's what I did).
Note: I haven't studied "poke" in any depth, but my guess is that it doesn't have anything like -returnClosure. However adding that should be easy as this:
if (-not $returnClosure) {
$methodInfo.Invoke($arguments)
} else {
{$methodInfo.Invoke($arguments)}.GetNewClosure()
}
Here's an - imperfect - implementation of a Stop-Pipeline cmdlet (requires PS v3+), gratefully adapted from this answer:
#requires -version 3
Filter Stop-Pipeline {
$sp = { Select-Object -First 1 }.GetSteppablePipeline($MyInvocation.CommandOrigin)
$sp.Begin($true)
$sp.Process(0)
}
# Example
1..5 | % { if ($_ -gt 2) { Stop-Pipeline }; $_ } # -> 1, 2
Caveat: I don't fully understand how it works, though fundamentally it takes advantage of Select -First's ability to stop the pipeline prematurely (PS v3+). However, in this case there is one crucial difference to how Select -First terminates the pipeline: downstream cmdlets (commands later in the pipeline) do not get a chance to run their end blocks.
Therefore, aggregating cmdlets (those that must receive all input before producing output, such as Sort-Object, Group-Object, and Measure-Object) will not produce output if placed later in the same pipeline; e.g.:
# !! NO output, because Sort-Object never finishes.
1..5 | % { if ($_ -gt 2) { Stop-Pipeline }; $_ } | Sort-Object
Background info that may lead to a better solution:
Thanks to PetSerAl, my answer here shows how to produce the same exception that Select-Object -First uses internally to stop upstream cmdlets.
However, there the exception is thrown from inside the cmdlet that is itself connected to the pipeline to stop, which is not the case here:
Stop-Pipeline, as used in the examples above, is not connected to the pipeline that should be stopped (only the enclosing ForEach-Object (%) block is), so the question is: How can the exception be thrown in the context of the target pipeline?
Try these filters, they'll force the pipeline to stop after the first object -or the first n elements- and store it -them- in a variable; you need to pass the name of the variable, if you don't the object(s) are pushed out but cannot be assigned to a variable.
filter FirstObject ([string]$vName = '') {
if ($vName) {sv $vName $_ -s 1} else {$_}
break
}
filter FirstElements ([int]$max = 2, [string]$vName = '') {
if ($max -le 0) {break} else {$_arr += ,$_}
if (!--$max) {
if ($vName) {sv $vName $_arr -s 1} else {$_arr}
break
}
}
# can't assign to a variable directly
$myLog = get-eventLog security | ... | firstObject
# pass the the varName
get-eventLog security | ... | firstObject myLog
$myLog
# can't assign to a variable directly
$myLogs = get-eventLog security | ... | firstElements 3
# pass the number of elements and the varName
get-eventLog security | ... | firstElements 3 myLogs
$myLogs
####################################
get-eventLog security | % {
if ($_.timegenerated -lt (date 11.09.08) -and`
$_.timegenerated -gt (date 11.01.08)) {$log1 = $_; break}
}
#
$log1
Another option would be to use the -file parameter on a switch statement. Using -file will read the file one line at a time, and you can use break to exit immediately without reading the rest of the file.
switch -file $someFile {
# Parse current line for later matches.
{ $script:line = [DateTime]$_ } { }
# If less than min date, keep looking.
{ $line -lt $minDate } { Write-Host "skipping: $line"; continue }
# If greater than max date, stop checking.
{ $line -gt $maxDate } { Write-Host "stopping: $line"; break }
# Otherwise, date is between min and max.
default { Write-Host "match: $line" }
}