15

Consider the following:

function OutputArray{
    $l = @(,(10,20))
    $l
}

(OutputArray) -is [collections.ienumerable]
# C:\ PS> True
(OutputArray).Count
# C:\ PS> 2

$l is "unrolled" when it enters the pipeline. This answer states that powershell unrolls all collections. A hashtable is a collection. However, a hashtable is of course unaffected by the pipeline:

function OutputHashtable{
    $h = @{nested=@{prop1=10;prop2=20}}
    $h
}

(OutputHashtable) -is [collections.ienumerable]
# C:\ PS> True
(OutputHashtable).Count
# C:\ PS> 1

This comment suggests that it is all IEnumerable that are converted to object arrays. However, both array and hashtable are ienumerable:

@(,(10,20)) -is [collections.ienumerable]
#True
@{nested=@{prop1=10;prop2=20}} -is [collections.ienumerable]
#True

What, exactly, are the conditions where powershell "unrolls" objects into the pipeline?

Community
  • 1
  • 1
alx9r
  • 3,675
  • 4
  • 26
  • 55
  • 1
    All `IEnumerable`s are obviously not converted to object arrays, so forget about that one. If that were true then `"apple"` should be unrolled to 5 items, since `string` is `IEnumerable`. But it is true that Powershell's exact rules for when something is one thing and when it's multiple things are arcane -- an *exact* answer would be interesting. – Jeroen Mostert Feb 24 '15 at 17:49
  • 1
    I'd say that the Powershell pipeline will "unroll", one level deep, all objects that have a Powershell type of array or collection. There will not always be a direct correlation between the adapted Powershell object type and the underlying dotnet type of the object, so determination of what type a particular object is needs to be kept in the context of Powershell object typing. – mjolinor Feb 24 '15 at 18:21
  • @mjolinor Can you explain what you mean by "Powershell type"? I've used `GetType()` and `Get-Member` to inspect a handful of types and all the results just point to the underlying dotnet types. – alx9r Feb 24 '15 at 19:55
  • @alx95 - look at a PS hashtable as an example. If you do a GetType() on it, it just shows "Hashtable", not "Collection", even though it's derived from the Collections class. That seems to be the gist of the question - why every PS type derived from the dotnet Collections class is not treated as a collection by the pipeline. – mjolinor Feb 24 '15 at 20:05
  • @mjolinor I see that, but `(New-Object System.Collections.BitArray -ArgumentList $true).GetType()` just shows "BitArray" not "Collection" in the same manner as `@{}.GetType()`. – alx9r Feb 24 '15 at 20:19
  • @alx9r OK. That implies Powershell sees it as an array type. Does the pipeline work with that? – mjolinor Feb 24 '15 at 20:30
  • Look at the PSObject for them. `(@{nested=@{prop1=10;prop2=20}}).psobject` shows a TypeName of System.Collections.Hashtable – TheMadTechnician Feb 24 '15 at 21:11

1 Answers1

10

Empirical Test Results

I'd rather have an analytical basis for these results, but I need an answer so I can move on. So, here are the results of my stab at an empirical test to discover which collections are unrolled by powershell's pipeline, and which aren't:

True in a column indicates there's probably some unrolling occurring.

StartingType                          ChangedInCmdlet^  ChangedWhenEmitted**
------------                          ---------------   ------------------
System.String                                           
System.Collections.ArrayList          True              True
System.Collections.BitArray           True              True
System.Collections.Hashtable
System.Collections.Queue              True              True
System.Collections.SortedList
System.Collections.Stack              True              True
System.Collections.Generic.Dictionary                   
System.Collections.Generic.List       True              True

These are results for a line of powershell that looks like this:

$result = $starting | Cmdlet

^ The ChangedInCmdlet column indicates that the type of $starting is different when it appears inside Cmdlet.

** The ChangedWhenEmitted column indicates that the type of $result is different when it is assigned to $result from when it was emitted inside Cmdlet.

There's probably some nuance in there for some types. That nuance can be analyzed by looking at the details of the output of the test script below. The whole test script is below.

Test Script

[System.Reflection.Assembly]::LoadWithPartialName('System.Collections') | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName('System.Collections.Generic') | Out-Null

Function BackThroughPipeline{
    [CmdletBinding()]
    param([parameter(position=1)]$InputObject)
    process{$InputObject}
}

Function EmitTypeName{
    [CmdletBinding()]
    param([parameter(ValueFromPipeline=$true)]$InputObject)
    process{$InputObject.GetType().FullName}
}

$objects = (New-Object string 'TenTwentyThirty'),
           ([System.Collections.ArrayList]@(10,20,30)),
           (New-Object System.Collections.BitArray 16),
           ([System.Collections.Hashtable]@{ten=10;twenty=20;thirty=30}),
           ([System.Collections.Queue]@(10,20,30)),
           ([System.Collections.SortedList]@{ten=10;twenty=20;thirty=30}),
           ([System.Collections.Stack]@(10,20,30)),
           (& {
               $d = New-Object "System.Collections.Generic.Dictionary``2[System.String,int32]"
               ('ten',10),('twenty',20),('thirty',30) | % {$d.Add($_[0],$_[1])}
               $d
           }),
           (& {
               $l = New-Object "System.Collections.Generic.List``1[int32]"
               10,20,30 | % {$l.Add($_)}
               $l
           })

$objects | 
    % {
        New-Object PSObject -Property @{
                StartingType  = $_.GetType().FullName
                StartingCount = $_.Count
                StartingItems = $_
                InCmdletType  = $_ | EmitTypeName
                InCmdletCount = ($_ | EmitTypeName).Count
                AfterCmdletType   = (BackThroughPipeline $_).GetType().FullName
                AfterCmdletItems  = (BackThroughPipeline $_)
                AfterCmdletCount  = (BackThroughPipeline $_).Count
                ChangedInCmdlet    = if ($_.GetType().FullName -ne ($_ | EmitTypeName) ) {$true};
                ChangedWhenEmitted = if (($_ | EmitTypeName) -ne (BackThroughPipeline $_).GetType().Fullname ) {$true}
            }
    }

Out-Collection Cmdlet

This testing eventually led me to create a cmdlet that conditionally wraps collections in sacrificial arrays to (hopefully) reliably prevent loop unrolling. That cmdlet is called Out-Collection and is in this github repository.

Community
  • 1
  • 1
alx9r
  • 3,675
  • 4
  • 26
  • 55
  • 2
    I'm worried a solid basis for an answer may well involve decompiling Powershell to see how its peculiar little brain is wired... but this is a nice effort regardless. – Jeroen Mostert Feb 24 '15 at 22:19
  • 2
    No need to dempile now that PowerShell is open source: https://github.com/PowerShell/PowerShell – Edward Mar 17 '17 at 01:02