Home > Mobile >  How to use a function to get objects from a pipeline as strings?
How to use a function to get objects from a pipeline as strings?

Time:01-24

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?

CodePudding user response:

As Abraham pointed out in his comment, you would need to capture all objects coming from the pipeline first and then output the object[] as a stream so that it is displayed properly on the console:

function f {
    [CmdletBinding()]
    param(
        [Parameter(
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        $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 from above, the example below would also work because of how the automatic variable $input works by enumerating the collection of all input to the function:

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

CodePudding user response:

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.

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) {
            $First = $False
            $Content |Out-String -Stream |Select-Object -SkipLast 1
        }
        else {
            $Content |Out-String -Stream |Select-Object -Skip 5 |Select-Object -SkipLast 1
        }
    }
}

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

CodePudding user response:

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.

  •  Tags:  
  • Related