8

Command that output the result in string instead of objects:

ls | Out-String -Stream

Output:

    Directory: C:\MyPath\dir1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2022-01-22  5:34 PM              0 1.txt
-a---          2022-01-22  5:34 PM              0 2.txt
-a---          2022-01-22  5:34 PM              0 3.txt

I tried to get the same result using a function:

function f {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline,
            ValueFromPipelineByPropertyName)]
        $Content
    )
    
    process {
        $Content | Out-String -Stream
    }
}

ls | f

However, the output is separated for each item:

    Directory: C:\MyPath\dir1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2022-01-22  5:34 PM              0 1.txt


    Directory: C:\MyPath\dir1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2022-01-22  5:34 PM              0 2.txt


    Directory: C:\MyPath\dir1

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2022-01-22  5:34 PM              0 3.txt

How can I use the function to get the same result as the first command?

iotop
  • 95
  • 1
  • 4
  • 4
    You would have to place it in either the begin, or end clause (*preferably begin*). This is because, the *process* block processes each item in the pipeline one at time. While the *begin* block would run just once, same for the *end* block which is usually used for cleanup. This kind of defeats the whole purpose in my opinion though, 'cause you just do what you're already doing. – Abraham Zinala Jan 23 '22 at 00:08

3 Answers3

10

As Abraham pointed out in his comment, you could capture all objects coming from the pipeline first and then output the objects as a stream so that it is displayed properly in the console.

It's important to note that both examples displayed below, are not truly "streaming functions", as mklement0 has pointed out in his helpful answer, both functions are first collecting all input coming from pipeline and then outputting the objects as a stream of strings at once, rather than object by object.

function f {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [object] $Content
    )

    begin { $output = [System.Collections.Generic.List[object]]::new() }
    process { $output.Add($Content) }
    end { $output | Out-String -Stream }
}

As an alternative to the advanced function posted above, the example below would also work because of how the automatic variable $input works in the end block by enumerating the collection of all input to the function:

function f { $input | Out-String -Stream }
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    The `$input`-based solution is simple and effective, but it's worth noting that it buffers all input up front. The first solution won't work as intended, because the `$Content` object is `.ToString()`-stringified rather than being passed to PowerShell's formatting system for stringification, which is what `Out-String` does. To wrap `Out-String -Stream` in true streaming fashion, you need a _proxy function_. – mklement0 Jan 23 '22 at 20:54
  • @mklement0 the first function was meant to not break the output rather than to replicate `Out-String -Stream`. I thought about a proxy command, but is copy pasting it a valid answer? – Santiago Squarzon Jan 23 '22 at 21:18
  • 1
    Well, the question is specifically about wrapping `Out-String -Stream` and getting the same output. You mean pasting the source code of `oss`? I think that's OK, as it allows studying the techniques used (as you've since noted, it is what I did in my answer) - and can serve as the basis for wrapping other commands. – mklement0 Jan 23 '22 at 21:28
  • 1
    @mklement0 following your first comment, as I see it, both functions in my answer are doing something similar (collecting all input from pipeline and passing the collection to `Out-String -Stream`) – Santiago Squarzon Jan 23 '22 at 21:36
  • 1
    Good point - for some reason I missed that you're calling `Out-String -Stream` in the `end` block. So +1 for that solution, which, as you note, is an advanced-function counterpart to the simple `$input`-based function, but the limitation that they share (which may not matter in practice) is their non-streaming nature. – mklement0 Jan 23 '22 at 22:00
  • 1
    @mklement0 oh yes, I totally agree with both function's limitation. I think OP's question is very valid if it's meant to understand how the pipeline works and why does `oss` behaves as it does, but I think iRon was on point with the educational statement _"I suspect that you either have a rare requirement or do not fully understand the PowerShell Pipeline..."_ – Santiago Squarzon Jan 23 '22 at 22:06
  • 1
    That's also a good point, but ultimately we can't know what the real use case is. The fact that the `oss` function is _included with PowerShell_ is a pointer that it has its uses and the prime candidate is to make up for `Select-String`'s useless behavior with _non-string_ input objects - see [this answer](https://stackoverflow.com/a/52615947/45375) – mklement0 Jan 23 '22 at 22:11
5

I think this question requires a counter question:

Why do you want to get objects from a pipeline as strings?

I suspect that you either have a rare requirement or do not fully understand the PowerShell Pipeline

In general, I would avoid using Out-String in the middle of the pipeline¹ as although it can be called from the middle of a pipeline, it already formats the output similar to Format-Table which is usually done at the end of the stream. The point is also that it is hard to determine the width of the columns upfront without knowing what comes next.

  1. With the exception mentioned by @mklement0:
    There's one good reason to use Out-String -Stream: to make up for Select-String's useless behavior with non-string input objects. In fact, PowerShell ships with proxy function oss, which wraps Out-String -Stream - see also: GitHub issue #10726 and his helpful answer.

The Out-String -Stream parameter also doesn't help for this as all it does is breaking the multiline string into separate strings:

By default, Out-String outputs a single string formatted as you would see it in the console including any blank headers or trailing newlines. The Stream parameter enables Out-String to output each line one by one. The only exception to this are multiline strings. In that case, Out-String will still output the string as a single, multiline string.

(ls  | Out-String -Stream).count
8

(ls  | Out-String -Stream)[3]
Mode                 LastWriteTime         Length Name

But if you really have a need to devaluate the PowerShell Objects (which are optimized for streaming) to strings (without choking the pipeline), you might actually do this:

function f {
    [CmdletBinding()] param ([Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]$Content)
    begin { $First = $True }
    process {
        if ($First) { $Content |Out-String -Stream |Select-Object -SkipLast 1 }
        else { $Content |Out-String -Stream |Select-Object -Skip 5 |Select-Object -SkipLast 1 }
        $First = $False
    }
}

It shows what you are are trying to do, but as said, I would really recommend against this if you do not have a very good reason. The usual way to do this and respecting the pipeline is placing the Out-String outside your cmdlet at the end of the stream:

ls |f |Out-String -Stream
iRon
  • 20,463
  • 10
  • 53
  • 79
4

As you've experienced, calling Out-String -Stream for each input object doesn't work as intended, because - aside from being inefficient - formatting each object in isolation invariably repeats the header in the (table-)formatted output.

The solutions in Santiago's helpful answer are effective, but have the disadvantage of collecting all pipeline input first, before processing it, as the following example demonstrates:

function f { $input | Out-String -Stream }

# !! Output doesn't appear until after the sleep period.
& { Get-Item $PROFILE; Start-Sleep 2; Get-Item $PROFILE } | f

Note: Output timing is one aspect, another is memory use; neither aspect may or may not matter in a given use case.


To wrap a cmdlet call in streaming fashion, where objects are processed as they become available, you need a so-called proxy (wrapper) function that utilizes a steppable pipeline.

In fact, PowerShell ships with an oss function that is a proxy function precisely around Out-String -Stream, as a convenient shortcut to the latter:

# Streaming behavior via the built-in proxy function oss:
# First output object appears *right away*.
& { Get-Item $PROFILE; Start-Sleep 2; Get-Item $PROFILE } | oss

Definition of proxy function oss (wraps Out-String -Stream); function body obtained with $function:oss:

function oss {

  [CmdletBinding()]
  param(
    [ValidateRange(2, 2147483647)]
    [int]
    ${Width},
  
    [Parameter(ValueFromPipeline = $true)]
    [psobject]
    ${InputObject})
  
  begin {
    $PSBoundParameters['Stream'] = $true
    $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Out-String', [System.Management.Automation.CommandTypes]::Cmdlet)
    $scriptCmd = { & $wrappedCmd @PSBoundParameters }
  
    $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
    $steppablePipeline.Begin($PSCmdlet)
  }
  
  process {
    $steppablePipeline.Process($_)
  }
  
  end {
    $steppablePipeline.End()
  }
  <#
  .ForwardHelpTargetName Out-String
  .ForwardHelpCategory Cmdlet
  #>
  
}

Note:

  • Most of the body is generated code - only the name of the wrapped cmdlet - Out-String - and its argument - -Stream - are specific to the function.

  • See this answer for more information.

mklement0
  • 382,024
  • 64
  • 607
  • 775