Pipe complete array-objects instead of array items one at a time?

43,198

Solution 1

Short answer: use unary array operator ,:

,$theArray | foreach{Write-Host $_}

Long answer: there is one thing you should understand about @() operator: it always interpret its content as statement, even if content is just an expression. Consider this code:

$a='A','B','C'
$b=@($a;)
$c=@($b;)

I add explicit end of statement mark ; here, although PowerShell allows to omit it. $a is array of three elements. What result of $a; statement? $a is a collection, so collection should be enumerated and each individual item should be passed by pipeline. So result of $a; statement is three elements written to pipeline. @($a;) see that three elements, but not the original array, and create array from them, so $b is array of three elements. Same way $c is array of same three elements. So when you write @($collection) you create array, that copy elements of $collection, instead of array of single element.

Solution 2

The comma character makes the data an array. In order to make the pipe line process your array as an array, instead of operating on each array element individually, you may also need to wrap the data with parentheses.

This is useful if you need to assess the status of multiple items in the array.

Using the following function

function funTest {
    param (
        [parameter(Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [alias ("Target")]
        [array]$Targets 
        ) # end param
    begin {}
    process {
        $RandomSeed = $( 1..1000 | Get-Random )
        foreach ($Target in $Targets) {
            Write-Host "$RandomSeed - $Target"
            } # next target
        } # end process
    end {}
    } # end function 

Consider the following examples:

Just Wrapping your array in parentheses does not guarantee the function will process the array of values in one process call. In this example we see the random number changes for each element in the array.

PS C:\> @(1,2,3,4,5) | funTest
153 - 1
87 - 2
96 - 3
96 - 4
986 - 5

Simply adding the leading comma, also does not guarantee the function will process the array of values in one process call. In this example we see the random number changes for each element in the array.

PS C:\> , 1,2,3,4,5 | funTest
1000 - 1
84 - 2
813 - 3
156 - 4
928 - 5

With the leading comma and the array of values in parentheses, we can see the random number stays the same because the function's foreach command is being leveraged.

PS C:\> , @( 1,2,3,4,5) | funTest
883 - 1
883 - 2
883 - 3
883 - 4
883 - 5

Solution 3

There's an old-school solution, if you don't mind that your process is a function.

Example: You want an array copied to the clipboard in a way that allows you to build it again on another system without any PSRemoting connectivity. So you want an array containing "A", "B", and "C" to transmute to a string: @("A","B","C") ...instead of a literal array.

So you build this (which isn't optimal for other reasons, but stay on topic):

# Serialize-List

param 
(
    [Parameter(Mandatory, ValueFromPipeline)]
    [string[]]$list
)
    $output = "@(";

    foreach ($element in $list)
    {
        $output += "`"$element`","
    }

    $output = $output.Substring(0, $output.Length - 1)
    $output += ")"
    $output

and it works when you specify the array as a parameter directly:

Serialize-List $list
@("A","B","C")

...but not so much when you pass it through the pipeline:

$list | Serialize-List
@("C")

But refactor your function with begin, process, and end blocks:

# Serialize-List

param 
(
    [Parameter(Mandatory, ValueFromPipeline)]
    [string[]]$list
)

begin
{
    $output = "@(";
}

process
{
    foreach ($element in $list)
    {
        $output += "`"$element`","
    }
}

end
{
    $output = $output.Substring(0, $output.Length - 1)
    $output += ")"
    $output
}

...and you get the desired output both ways.

Serialize-List $list
@("A","B","C")

$list | Serialize-List
@("A","B","C")

Solution 4

The more "correct" way would be to use the Write-Output cmdlet and specify the -NoEnumerate switch:

Write-Output $theArray -NoEnumerate | Do-SomeStuff

Also, the author states:

I have a second way that's more of a hack (and I try to avoid hacks like this). You can place a comma in front of the array before you pipe it.

Both will work, but using the comma operator will always create an additional array to contain the original one, whereas Write-Output -NoEnumerate will write the original array to the pipeline in one step.

Share:
43,198
NoOneSpecial
Author by

NoOneSpecial

Updated on October 27, 2021

Comments

  • NoOneSpecial
    NoOneSpecial over 2 years

    How do you send the output from one CmdLet to the next one in a pipeline as a complete array-object instead of the individual items in the array one at a time?

    The problem - Generic description
    As can be seen in help for about_pipelines (help pipeline) powershell sends objects one at the time down the pipeline¹. So Get-Process -Name notepad | Stop-Process sends one process at the time down the pipe.

    Lets say we have a 3rd party CmdLet (Do-SomeStuff) that can't be modified or changed in any way. Do-SomeStuff perform different if it is passed an array of strings or if it is passed a single string object.

    Do-SomeStuff is just an example, it could be substituted for ForEach-Object, Select-Object, Write-Host (or any other CmdLet accepting pipeline input)

    Do-SomeStuff will in this example process the individual items in the array one at the time.

    $theArray = @("A", "B", "C")
    $theArray | Do-SomeStuff
    

    If we want to send the complete array as one object to Do-SomeStuff one might try something like this

    @($theArray) | Do-SomeStuff
    

    But it does not produce the expected result since PowerShell "ignores" the new single-item-array.

    So, how do you "force" $theArray to be passed down the pipe as a single array-object instead of the content items one at the time?


    The problem - practical example
    As shown below the output of Write-Host is different if passed an array or if it passed the individual items in the array one at the time.

    PS C:\> $theArray = @("A", "B", "C")
    PS C:\> Write-Host $theArray
    A B C
    PS C:\> $theArray | foreach{Write-Host $_}
    A
    B
    C
    PS C:\> @($theArray) | foreach{Write-Host $_}
    A
    B
    C
    

    How do you do to get $theArray | foreach{Write-Host $_} to produce the same output as Write-Host $theArray ?




    FOOTNOTES

    1. Pipeline processing in Powershell

    A normal array of strings

    PS C:\> @("A", "B", "C").GetType().FullName
    System.Object[]
    


    A normal array of strings piped to Foreach-Object

    PS C:\> @("A", "B", "C") | foreach{$_.GetType().FullName}
    System.String
    System.String
    System.String
    

    Each string in the array is processed one at the time by the ForEach-Object CmdLet.


    An array of arrays, where the "inner" arrays are arrays of strings.

    PS C:\> @(@("A", "B", "C"), @("D", "E", "F"), @("G", "H", "I")) | foreach{$_.GetType().FullName}
    System.Object[]
    System.Object[]
    System.Object[]
    

    Each array in the array is processed one at the time by the ForEach-Object CmdLet, and the content of each sub-array from the input is handled as one object even though it is an array.