Powershell: Select-Object -First X skips End{ } blocks of previous cmdlets

Created on 3 Oct 2018  路  6Comments  路  Source: PowerShell/PowerShell

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.

Steps to reproduce

(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

Expected behavior

Begin Test
Begin Test
Process Test
Process Test
1
Process Test
Process Test
2
End Test
End Test

Actual behavior

Begin test
Begin test
Process test
Process test
1
Process test
Process test
2

Environment data

> $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
Issue-Discussion WG-Engine

Most helpful comment

This issue would be alleviated somewhat with the introduction of cleanup{} in this PR: #9900

All 6 comments

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 throwing 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

Was this page helpful?
0 / 5 - 0 ratings