-
Notifications
You must be signed in to change notification settings - Fork 7.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tee-Object -Process #20123
Comments
YES The more I've pushed this around, the more I think you need to have a second pipeline. " I've recycled some stuff I know from Proxy functions to create and run a steppable pipeline as a "Tee" or "Fork". There is probably a more elegant way of preventing the that pipeline sending data into the following command. but this is just a p.o.c :-) function Invoke-Fork {
param(
[Parameter(Position=0,Mandatory=$true)]
[scriptblock]$forkBlock ,
[Parameter(ValueFromPipeline=$true)]
$InputObect
)
begin { try {
$steppablePipeline = $forkBlock.GetSteppablePipeline($myInvocation.CommandOrigin)
$null = $steppablePipeline.Begin($PSCmdlet) }
catch { throw }
}
process { try {$null = $steppablePipeline.Process($_) ; Write-Output $_ } catch { throw}}
end { try {$null = $steppablePipeline.End() } catch { throw }}
clean { if ($null -ne $steppablePipeline) { $null = $steppablePipeline.Clean() } }
} So this works
and the block can be a multi-command pipeline
My addition of
Write-host works - what gets printed when is harder to follow.
Roughly what you're looking to do ? |
@jhoneill Will have to play around it, but this looks amazing 🤩 |
A steppable pipeline looks promising, but note that PS> 1..3 | Invoke-Fork { % { -$_ } }
-1
1
-2
2
-3
3 Assuming it is possible to capture the output from a steppable pipeline (which should then be a separate one), the logic could be as follows:
|
@mklement0 How is |
Good question, @dkaszews. It uses the PowerShell/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TimeExpressionCommand.cs Lines 62 to 69 in 14bd9c7
Note that the script block is invoked for every input object, with 1..3 | Measure-Command { $_ | Out-Host; pause } |
@mklement0 So while the PowerShell PoC is not able to replicate it, it should be relatively easy to do in the core C#? The whole idea behind this proposal was to replicate some of the stuff you can do in Linux with Edit: sorry, I missed the last part. So the |
@dkaszews, if I understand correctly, what is needed is a steppable pipeline that is separate from the pipeline in which the enclosing command runs, is invoked input object by input object from the enclosing command's I'm not familiar enough with the SDK to know if this possible, but perhaps @SeeminglyScience can weigh in. |
That's odd. I posted a version of "invoke fork" with a kludged on addition of "| out-Null" (I referred to that here #20133 (comment) ) but it seems to have gone in editing the post. it was I think there may be an important difference with Measure-Command because that isn't starting a pipe into which objects can be pushed. |
Good point - I suspect there is a more elegant way of creating the steppable script block with the added pipeline segments than basing it on the string representation of the input script block, which could have side effects. Note that the Thus, you can do the following: $objects = [pscustomobject] @{ Num = 1 }, [pscustomobject] @{ Num = 2 }
# Neither -FilePath nor -Variable specified: script-block output, if any, is discarded.
$objects | Tee-WithProcess { Export-Csv out.csv } |
Out-Host
'---'
# Combination with the -Host switch: print the output to the host (display).
$objects | Tee-WithProcess { Format-List } -Host |
Out-Host
'---'
# -FilePath: Output (via Out-File) to the specified file
# UPDATE: The p.o.c now treats a script block that doesn't contain a *single command*
# or single pipeline that *starts with a command* implicitly as if it were enclosed
# in `ForEach-Object`, so no explicit use of the latter is needed anymore.
$objects | Tee-WithProcess { -$_.Num } -FilePath out.txt |
Out-Host
'---'
# -Variable: Output is collected in the specified variable ($out)
$objects | Tee-WithProcess { -$_.Num } -Variable out |
Out-Host
function Tee-WithProcess {
param(
[Parameter(Position = 0, Mandatory)]
[scriptblock] $Process,
[Parameter(ValueFromPipeline)]
$InputObject,
[string] $Variable,
[string] $FilePath,
[Alias('Host')] # Note: In a function implementation $Host cannot be used.
[switch] $ToHost
)
begin {
try {
# Rule out mutually exclusive switches.
if ((0, 1)[[bool] $Variable] + (0, 1)[[bool] $FilePath] + $ToHost.IsPresent -gt 1) { throw "-Variable, -FilePath, and -Host are mutually exclusive." }
# Determine if the content of the script block is a single that statement comprising
# either a single *command* or a single pipeline that *starts with* a command
# which is assumed to *directly receive all pipeline input* (e.g, `Export-Csv`).
# If not, assume it represents code to be run *for each input object*,
# with $_ reflecting each, and therefore wrap it in a ForEach-Object call.
$isCommandPipeline = ($Process.Ast.EndBlock.Statements.Count -eq 1 -and $Process.Ast.EndBlock.Statements[0].PipelineElements[0] -is [System.Management.Automation.Language.CommandAst])
# NOTE: The implementation the block for the steppable pipeline via
# recreation of the script block from its *string representation* in the single-command case
# is SUBOPTIMAL, but was chosen for simplicity in this proof-of-concept.
$steppableScriptBlock =
[scriptblock]::Create($(
if ($ToHost) {
$isCommandPipeline ?
[scriptblock]::Create("$Process | Out-Host")
:
{ ForEach-Object -Process $Process | Out-Host }
} elseif ($FilePath) {
$isCommandPipeline ?
[scriptblock]::Create("$Process | Out-File -LiteralPath `"$FilePath`"")
:
{ ForEach-Object -Process $Process | Out-File -LiteralPath $FilePath }
} elseif ($Variable) {
$isCommandPipeline ?
[scriptblock]::Create("$Process | Write-Output -NoEnumerate -OutVariable aux | ForEach-Object -Process {} -End { Set-Variable -Scope 1 `"$Variable`" -Value `$aux }")
:
{ ForEach-Object -Process $Process | Write-Output -NoEnumerate -OutVariable aux | ForEach-Object -Process {} -End { Set-Variable -Scope 1 $Variable -Value $aux } }
} else {
$isCommandPipeline ?
[scriptblock]::Create("$Process | Out-Null")
:
{ ForEach-Object -Process $Process | Out-Null }
}
))
$steppablePipeline = $steppableScriptBlock.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
catch { $PSCmdlet.ThrowTerminatingError($_) }
}
process { try { $steppablePipeline.Process($InputObject) } catch { $PSCmdlet.ThrowTerminatingError($_) }; $InputObject }
end { try { $steppablePipeline.End() } catch { $PSCmdlet.ThrowTerminatingError($_) } }
clean { if ($null -ne $steppablePipeline) { $steppablePipeline.Clean() } }
} |
Any reason why you need |
Thanks for catching that, @dkaszews. You do need a |
I played around trying to rebind the output pipe to either null, file or variable without converting the scriptblock to string and back, but no dice. Maybe it is solveable with a smarter wrapper, maybe it is not possible without delving into engine internals. I poked around For the testing, I used the following to distinguish the host and pipe output, it nicely shows whether the 1..3 |
Tee-WithProcess { %{ Write-Host 'Invoked'; 'Leaked' } } |
ConvertTo-Json -Compress |
Should -Be '[1,2,3]' |
That's not going to work . At the very least it would to be The code I thought I originally posted function Invoke-Fork {
param(
[Parameter(Position=0,Mandatory=$true)]
$ForkBlock,
[Parameter(ValueFromPipeline=$true)]
$InputObect
)
begin {try {
$ForkBlock = [scriptblock]::Create( $forkBlock.ToString() + " | Out-Null" )
$steppablePipeline = $ForkBlock.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet) }
catch {throw }
}
process {try {$steppablePipeline.Process($_) ; Write-Output $_} catch {throw}}
end {try {$steppablePipeline.End() } catch { throw }}
clean {if ($steppablePipeline) { $null = $steppablePipeline.Clean() } }
} Passes your test but with the convert to/from string
I changed the begin block #$ForkBlock = [scriptblock]::Create( $forkBlock.ToString() + " | Out-Null" )
$F2 = { Foreach-object -Process { & $ForkBlock | Out-Null}}
$steppablePipeline = $F2.GetSteppablePipeline($myInvocation.CommandOrigin) That works without the round-trip to string and back. |
I wonder if it makes sence to accept a ... | Tee-Object -Process Name -FilePath out.txt | ... |
@iRon7, I like the idea of simplifying with a string, but the question is what the string should represent. I just updated the proof-of-concept with the future # "Tee" the Format-List output to the host.
$date = Get-Date | Tee-WithProcess { Format-List } -Host You could argue that being able to pass # Possibly allow specifying a parameter-less command name as a *string*.
$date = Get-Date | Tee-WithProcess Format-List -Host While a simple way to access a property (or several) on the input objects is appealing too, it would call for a new Sticking with just Taking a step back: Conceivably, there could be two closely related, mutually exclusive parameters (names negotiable):
$objects | Tee-WithProcess -ProcessCommand { Export-Csv out.csv }
$objects | Tee-WithProcess -ProcessCommand Format-List -Host # short for: -ProcessCommand { Format-List }
$objects | Tee-WithProcess -ProcessEach { "Processing $($_.Num)..." } -Host
$objects | Tee-WithProcess -ProcessEach Num -Variable numbers # short for: -ProcessEach { $_.Num } |
I was just thinking the same and even possibly also supporting multiple properties, this would than result in a scalar
or an object in case an array is supplied as argument, e.g.:
You might even think of calculated properties here, but as you said, it may might pack too much into the command |
To test some of the approaches, I have added > $ProcessSimple = { %{ Write-Host "Tee-Process process: $_"; "Tee-Process return: $_" } }
> $ProcessAdvanced = {
begin { Write-Host 'Tee-Process begin' }
process { Write-Host "Tee-Process process: $_"; "Tee-Process return: $_" }
end { Write-Host 'Tee-Process end' }
}
> $ProcessContent = { Set-Content 'process.log' } I then created different versions of the
|
I did couple more experiments to try to replicate the errors shown above with the following scriptblocks, not wrapped in $ProcessSingle = { Write-Host 'test' }
$ProcessMulti = { Write-Host $_; $_ } With version 1. and 2. I got the following two results: > 1..3 | Tee-WithProcess $ProcessSingle | ConvertTo-Json -Compress
Tee-WithProcess: The input object cannot be bound to any parameters for the command either because the command d
oes not take pipeline input or the input and its properties do not match any of the parameters that take pipelin
e input.
> 1..3 | Tee-WithProcess $ProcessMulti | ConvertTo-Json -Compress
Tee-WithProcess: The variable '$steppablePipeline' cannot be retrieved because it has not been set.
Tee-WithProcess: Exception calling "GetSteppablePipeline" with "1" argument(s): "Only a script block that contai
ns exactly one pipeline or command can be converted. Expressions or control structures are not permitted. Verify
that the script block contains exactly one pipeline or command." The first one is relatively clear - raw scriptblock does not accept pipeline input by default, which is why it errors on With version 3. and 4. which wrap the > 1..3 | Tee-WithProcess $ProcessSingle | ConvertTo-Json -Compress
test
test
test
[1,2,3]
> 1..3 | Tee-WithProcess $ProcessMulti | ConvertTo-Json -Compress
1
2
3
[1,2,3] |
I did a test regarding open 4., and unfortunately, as expected, there is nothing to stop the side-process from mutating the objects passing through, unless we deep copy them which would likely kill performance. Man, do I wish every language had C++'s > 1..3 | %{ @{ x = $_ } } | Tee-WithProcess { %{ $_['x'] = 0 } } | ConvertTo-Json -Compress
[{"x":0},{"x":0},{"x":0}]
> 1..3 | %{ @{ x = $_ } } | ConvertTo-Json -Compress
[{"x":1},{"x":2},{"x":3}] The question now is, if we have to trust the |
That's the equivalent of Is
If you want that you'll need to do it with
Depending on whether I think, though, if it didn't catch "stray" object and people had to put their own Out-Null in, the first change that would be requested would be to make Out-Null automatic. |
@iRon7, especially with a If you want multiple properties with (As an aside: @dkaszews, regarding not being to prevent mutation of the teed objects:
E.g.: # Equivalent of:
# $objects | ForEach-Object { $_.Num++; $_ } | Export-Csv out.csv
$objects | Tee-Object { $_.Num++ } | Export-Csv out.csv I'll elaborate on the With the E.g., Thus, your Switching the implementation of calling the |
Based on the above it occurred to me that there's no need for distinct I've updated the proof-of-concept accordingly, so that the following now works: # Single-command case
$objects | Tee-WithProcess { Export-Csv out.csv }
# Expression case with implicit ForEach-Object
$result = $objects | Tee-WithProcess { 'Processing: ' + $_.Num } -Host In the rare event that a single command is passed that should be called for each object, simply wrap it in However, with the return to a single
|
Thanks, I was confused because I rewrote it to: $ProcessForeach = {
ForEach-Object `
-begin { Write-Host 'Tee-Process begin' } `
-process { Write-Host "Tee-Process process: $_"; "Tee-Process return: $_" } `
-end { Write-Host 'Tee-Process end' }
} and now it works correctly with version 2. (strings), but fails with 3. and 4. due to extra > 1..3 | Tee-WithProcess -Version 1 $ProcessForeach | ConvertTo-Json -Compress
Tee-Process begin
Tee-Process process: 1
Tee-Process process: 2
Tee-Process process: 3
Tee-Process end
["Tee-Process return: 1",1,"Tee-Process return: 2",2,"Tee-Process return: 3",3]
> 1..3 | Tee-WithProcess -Version 2 $ProcessForeach | ConvertTo-Json -Compress
Tee-Process begin
Tee-Process process: 1
Tee-Process process: 2
Tee-Process process: 3
Tee-Process end
[1,2,3]
> 1..3 | Tee-WithProcess -Version 3 $ProcessForeach | ConvertTo-Json -Compress
Tee-Process begin
Tee-Process process: 1
Tee-Process end
Tee-Process begin
Tee-Process process: 2
Tee-Process end
Tee-Process begin
Tee-Process process: 3
Tee-Process end
[1,2,3]
> 1..3 | Tee-WithProcess -Version 4 $ProcessForeach | ConvertTo-Json -Compress
Tee-Process begin
Tee-Process process: 1
Tee-Process end
Tee-Process begin
Tee-Process process: 2
Tee-Process end
Tee-Process begin
Tee-Process process: 3
Tee-Process end
[1,2,3]
I tried swapping around
Sure, it's just that I don't a way to implement without @mklement0's string hackery, which I am afraid will have some odd side effects, and definitely performance hit of having to reparse the scriptblock. In my ideal world, there would be a method
Sure, it has to be documented, but same is for I agree with @mklement0, I think strings would be confusing, especially since without any of |
Whoa, did not know that > $ProcessAdvanced.Ast
BeginBlock : begin { Write-Host 'Tee-Process begin' }
ProcessBlock : process { Write-Host "Tee-Process process: $_"; "Tee-Process return: $_" }
EndBlock : end { Write-Host 'Tee-Process end' }
> $ProcessForeach.Ast
BeginBlock :
ProcessBlock :
EndBlock : ForEach-Object `
-begin { Write-Host 'Tee-Process begin' } `
-process { Write-Host "Tee-Process process: $_"; "Tee-Process return: $_" } `
-end { Write-Host 'Tee-Process end' } We can distinguish advanced and simple scriptblocks, because the latter only have But also it seems like we should be able to add > $Noisy = { %{ Write-Host "Processing: $_"; Write-Output "Returing: $_" } }
> $Quiet = { %{ Write-Host "Processing: $_"; Write-Output "Returing: $_" } | Out-Null }
> 1..3 | Tee-WithProcess -Version 1 $Noisy | ConvertTo-Json -Compress
Processing: 1
Processing: 2
Processing: 3
[1,"Returing: 1",2,"Returing: 2",3,"Returing: 3"]
> 1..3 | Tee-WithProcess -Version 1 $Quiet | ConvertTo-Json -Compress
Processing: 1
Processing: 2
Processing: 3
[1,2,3]
> $Noisy.Ast.EndBlock.Statements[0].PipelineElements.Extent.Text
%{ Write-Host "Processing: $_"; Write-Output "Returing: $_" }
> $Quiet.Ast.EndBlock.Statements[0].PipelineElements.Extent.Text
%{ Write-Host "Processing: $_"; Write-Output "Returing: $_" }
Out-Null This shows that simply appending
While everything under |
@dkaszews, indeed the recreate-the-script-block-from-its-string-representation problem needs solving, and it is now half solved: in the case where Good pointers re AST; it's definitely possible to create a new AST that builds on an existing one, and then call |
If you want to pass an advanced script block to the updated p.o.c (which I imagine will rarely be necessary in practice), this is how you would do it ( 1..3 | Tee-WithProcess { . { begin { 'begin' } process { 10 * $_ } end { 'end' } } } -Host Output (mix of host and success output):
|
@dkaszews This getting harder to follow, but
The bit about Write-object does seem to be wrong. function a {
# Sends output in all 3 blocks
begin {Write-host " A begins" ; 0 }
process {Write-host " A process" ; 1,2,3}
end {Write-host " A end" ; 4 }
}
function b { param ([Parameter(ValueFromPipeline)]$P)
begin {Write-host " b begins '$p'" }
process {Write-host " b process '$p'"; write-output $p #try $p or write-output $$P
#if ($p -eq 2 ) {throw} # test what happens to the others if a fatal error happens in the middle.
}
end {Write-host " b end $P"}
}
function c { param ([Parameter(ValueFromPipeline)]$P)
begin {Write-host " c begins '$p'" }
process {Write-host " c process '$p'" ; $P}
end {Write-host " c end $P"; }
}
a | b | c Whether B uses
Note that A sending output in its begin block changes the order of begin and first process. Otherwise it is
As a side thing functions which don't have begin / process /end run as an end block so
Filters seem to have fallen out of use but a filter which doesn't specify begin / process /end run as a process block
|
@PowerShell/wg-powershell-cmdlets discussed this and agree on the utility of adding this capability, however, we have concerns about how this will work in practice with live .NET objects. We defer to the @PowerShell/wg-powershell-engine group to discuss. |
We discussed this in the Engine WG but the more I look through the thread the more unsure I am about what the actual ask is. It seems like it's either:
Are either of these correct? If not, can someone distill the discussion into a proposal we can discuss? |
From my perspective, the proposal is as follows - I hope @dkaszews and @iRon7 agree:
Examples and a proof-of-concept are in this comment. The open questions are:
|
Just a thought: why not multiple? |
@iRon7, I assume you mean supporting multiple script blocks, i.e. to type That sounds like a nice enhancement, though we'd have to make it clear how that behaves in combination with a teeing target: if I understand you correctly, we'd then get: 1..3 | Tee-Object -Process { Write-Output }, { $_ * 10 } -Host | Out-Null
1
10
2
20
3
30 |
No, what I meant is instead of a single pipeline, for each main process step check if the concerned pipeline is already started,
And at the end close all the Basically as in these examples: |
Prototype of what I had in mind: function TeeObject {
param([ScriptBlock]$Process)
begin {
$Pipeline = @{}
}
process {
$Key = $ExecutionContext.InvokeCommand.ExpandString($Process.ToString())
if (!$Pipeline.Contains($Key)) {
$Pipeline[$Key] = $Process.GetSteppablePipeline()
$Pipeline[$Key].Begin($True)
}
$Pipeline[$Key].Process($_)
$_ # pass on the current item
}
end {
$Pipeline.get_Values().End()
}
}
$Csv = @'
name, account, Appname, count, users, List
name1, account1, 123, 1, 2, abc
name1, account2, 123, 2, 3, z
name1, account3, 123, 3, 6, xc
name1, account4, 123, 5, 7, df
name1, account5, 123, 5, 8, rg
name1, account6, 123, 7, 0, sfg
name2, account1, 456, 1, 1, dfg
name3, account1, 789, 3, 7, rt
name3, account2, 789, 7, 1, ert
'@
$Csv | ConvertFrom-Csv | TeeObject -Process { Export-Csv .\$($_.Appname).Csv } | Out-Null |
@iRon7, while I can see the appeal of such functionality in principle, I don't think the implementation you're proposing is the way to go (and I'm not sure there is an alternative way that fits into this proposal, which doesn't preclude thinking about it separately, however):
|
(misclick fyi) |
Tee-Object for two? Knife and fork? Why limit yourself to two? As @iRon7 says
Invoke-PipelineBroadcast runs multiple parallel script blocks and feeds the input pipeline records to each. |
@rhubarb-geek-nz, that's an interesting cmdlet, but it serves a different use case: sending a single pipeline's input to multiple, independent script blocks for output-less processing. Here, we are talking about extension to |
I have added a PassThru argument so the input pipeline can optionally go directly to the output pipeline in addition to all the script blocks. The script blocks can also do their own pipelines and outputs. Perhaps I should have called it Tee-Party |
Tee-Party it is.
|
Summary of the new feature / enhancement
Tee-Object
could use a new parameterProcess
, accepting a scriptblock which will be invoked for every item going through it. Example usecases:Tee-Object
should have a-Console
parameter #19827 -... | Tee-Object -Host | ...
could be written as... | Tee-Object -Process { Write-Host } | ...
(bit verbose, soTee-Object
should have a-Console
parameter #19827 on its own is probably still a good idea)-PassThru
as common parameter #19989 -... | ExportCsv files.csv -PassThru | ...
could be written as... | Tee-Object -Process { Export-Csv files.csv } | ...
(saves on adding-PassThru
to each missing cmdlet#19989
, add-Format
parameter toWrite-Host
#20121 -... | Write-Host -PassThru -Format "Processing: $_" | ...
could be written as... | Tee-Object -Process { Write-Host "Processing: $_" }
Proposed technical implementation details (optional)
Opens:
Begin
,Process
,End
? Without it, we may either accidentally call the cmdlet for each object separately, causing each of them to clobber the last. This means we cannot simply implement it asForEach-Item { $_ | $Process; $_ }
:ForEach-Object -Begin { $pipe = [List[object]]:new() } -Process { $pipe.Add($_); $_ } -End { $pipe | $Process }
. It will be especially visible with usecase 1.:Tee-Object -Process { Write-Host }
will only start outputting once the main pipeline has finished.ForEach-Object
, the first is true, as can be seen with0..4 | %{ $_; $_ } | Measure-Object
outputting10
.Process
? Secondary pipe then primary, other way around, concurrent, undefined?$_
and pretending all the items are piped from the left even legal or reasonable? I don't see how to express some of the intentions I have shown without it, unless we make$_
inTee-Object -Process { $_ | Export-Csv files.csv }
mean the entire pipeline. Otherwise, maybe some new symbol like$%
would make sense, as%
is commonly already associated with item processing as default alias forForEach-Object
.The text was updated successfully, but these errors were encountered: