Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to explain not working "Tee-Object" when "Select-Object -First" occurs

With the following code, $t equals @(1,2).

$t = "before"
1..2 | Tee-Object -Variable t

So why is it that the next code snippet has $t equal to "before" instead of @(1)*?

$t = "before"
1..2 | Tee-Object -Variable t | Select-Object -First 1

I see the same result in Powershell version 5 and version 3.

like image 577
jjacek Avatar asked Feb 26 '16 17:02

jjacek


1 Answers

This has to do with how the pipeline works. If you use a different cmdlet, like Write-Verbose it will work as expected:

$t = "before"
1..2 | Tee-Object -Variable t | Write-Verbose -Verbose

Consider that during a pipeline, each function or cmdlet can use a Begin,Process, and End block.

All the Begin blocks run first, then Process is called once for each pipeline object, and then all the Ends.

The variable assignment necessarily has to happen in the End block.

Starting in PowerShell v3, cmdlets can interrupt the pipeline.

This is great for something like Select-Object -First 1 because it means the entire pipeline can be stopped after the first object, instead of executing once for each item even though the rest will be discarded.

But it also means that the End block never runs.

If you start powershell in v2: powershell.exe -Version 2.0

And then run your second example, it will work as expected because the pipeline couldn't be prematurely stopped in that version.


Here's a demonstration:

function F1 {
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline)]
    $o
)

    Begin {
        Write-Verbose "Begin" -Verbose
    }

    Process {
        Write-Verbose $o -Verbose
        $o
    }

    End {
        Write-Verbose "End" -Verbose
    }
}

Then call it:

1..2 | F1

vs.

1..2 | F1 | Select-Object -First 1

You could also demonstrate this with ForEach-Object:

1..2 | ForEach-Object -Begin {
    Write-Verbose "Begin" -Verbose
} -Process {
    Write-Verbose $_ -Verbose
    $_
} -End {
    Write-Verbose "End" -Verbose
}

vs.

1..2 | ForEach-Object -Begin {
    Write-Verbose "Begin" -Verbose
} -Process {
    Write-Verbose $_ -Verbose
    $_
} -End {
    Write-Verbose "End" -Verbose
} | Select-Object -First 1

According to the documentation you linked, you can use the -Wait parameter to turn off this optimization:

$t = "before"
1..2 | Tee-Object -Variable t | Select-Object -First 1 -Wait

This will populate $t but perhaps not with the value you wanted. It will contain @(1,2), presumably since the -OutVariable was placed on Tee-Object and not on Select-Object.

Remember that the "result" of the pipeline is what gets returned from the execution (left side of the =), and that is correct in all instances.

-OutVariable is something implemented by some cmdlets, and it most likely has to be implemented in the End block for that particular cmdlet, so to predict what it will give is highly dependent on understanding the execution flow of the pipeline.

So to answer the question in your comment, it appears to me to be implemented correctly. Am I misunderstanding your assertion?

like image 113
briantist Avatar answered Nov 12 '22 04:11

briantist