Using the -First $n
parameter for Select-Object
completely skips the end {}
blocks of all cmdlets preceding it in the pipeline.
This can be quite hazardous, e.g., for a cmdlet that access a resource that needs to have that handle properly disposed of (like a file) may not release that handle correctly.
There is no finally {}
block available to PS, and the method in which Select-Object
terminates the pipeline appears to happen in such a manner that no script-based function can account for; that is, even with a try...finally
construct in the cmdlets' process {}
blocks it will not kick off in the event that Select-Object
is used to prematurely end the pipeline.
Instead, a method should be exposed for Select-Object
(and potentially other cmdlets?) to safely terminate the pipeline by ceasing all process
steps and kicking off the chain of end {}
steps in the pipeline.
(shamelessly pinched from @TimCurwick's fantastic example he posted in the PS Slack)
function test {
[cmdletbinding()]
param(
[Parameter(ValueFromPipeline)]
$X
)
begin {
Write-Host "Begin test"
}
process {
Write-Host "Process test"
$X
}
end {
Write-Host "End test"
}
}
1, 2, 3 | test | test | select -first 2
Begin Test
Begin Test
Process Test
Process Test
1
Process Test
Process Test
2
End Test
End Test
Begin test
Begin test
Process test
Process test
1
Process test
Process test
2
> $PSVersionTable
Name Value
---- -----
PSVersion 6.1.0
PSEdition Core
GitCommitId 6.1.0
OS Microsoft Windows 10.0.17763
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Hi @vexx32 end
is not intended to be a cleanup block. Cleanups in cmdlets should be handled in the Dispose()
implementation. For scripts, there currently isn't an equivalent solution. Issue #6673 was opened to address this.
Certainly, @BrucePay but given that output from end{}
is valid and an accepted pattern, there exist many cases with Select-Object -First $n
where that output (which may be perfectly valid regardless of the decision to accept only the first $n
items from process {}
may not occur.
There may also be logging steps or any number of actions that one would expect to be performed in the end {}
step which is, silently and without any form of documentation explaining the behaviour, completely skipped without warning. There is one line in the Select-Object documentation that indicates that it stops prior commands. This is, however, without mention that potentially significant portions of all previous commands in the entire pipeline sequence preceding Select-Object
will be summarily skipped. In a sense, it feels a bit like a breach of the pipeline contract, so to speak, if there was such a thing.
The worst part is, because this is implemented in this fashion, there is absolutely no way for a module author to combat this. Select-Object -First $n
is an effective way of completely neutering any cmdlet's end {}
block, whether that is your intention or not.
Appreciate the linked issue, but I think that given the pipeline standard of executing begin {}
once, process {}
for each object, and end {}
once, it simply doesn't make sense for a cmdlet from another module to entirely and without any kind of warning for the user skip entire segments of other cmdlets' or functions' code.
@vexx32
but given that output from end{} is valid and an accepted pattern, there exist many cases with Select-Object -First $n where that output (which may be perfectly valid regardless of the decision to accept only the first $n items from process {} may not occur
-first n
stops after n characters have been written. _That's the literal definition of -first
_. It doesn't matter if those characters come from process
or end
, you ask for n characters, you get n characters. Very simple. And there has never been any contract that says all pieces of the pipeline will execute fully. Any cmdlet can throw a pipeline-terminating exception, or just a terminating exception or the user can hit ctrl-c. Things are guaranteed be called in begin
,process
,end
order but not necessarily that they will all be called.
Sure, in cases of error, not everything happens. But Select-Object abusing that means that any sane user who utilises that (awfully handy) functionality remains unaware that code has been skipped -- until something else goes wrong further down the line.
In cases of error, this behaviour is relatively obvious. In Select-Object's usage? It's not an error state, it's a deliberately induced state that has unforeseeable consequences unless one is familiar with the underlying code, and shouldn't directly alter other functions' code paths like this.
I agree that from an end user's perspective there's a crucial difference between something throw
ing in a pipeline - an unexpected _error_ that, if unhandled, _aborts processing_ - and using Select-Object -First
to only process part of the pipeline input - a regular feature during _normal operation_ that simplify _modifies_ processing.
It just so happens that the way Select-Object -First
stops the upstream cmdlets is also implemented as an exception _behind the scenes_, but that is an implementation detail that shouldn't determine the behavior, for the reasons @vexx32 has already stated.
This issue would be alleviated somewhat with the introduction of cleanup{}
in this PR: #9900
Most helpful comment
This issue would be alleviated somewhat with the introduction of
cleanup{}
in this PR: #9900