arrayspowershellcollectionspowershell-cmdlet

A cmdlet which returns multiple objects, what collection type is it, if any? [PowerShell]


An example of the Get-ADuser cmdlet:

$Users = Get-ADuser -Filter *

It will in most cases return multiple ADuser objects, but what "collection" type is it? The documentation only says it will returns one or more user objects of Microsoft.ActiveDirectory.Management.ADUser.

Tried to use e.g. ($Users -is [System.Collections.ArrayList]) but I cannot nail the "collection" type?


Solution

  • Cmdlets themselves typically use no collection type in their output.[1]:
    They emit individual objects to the pipeline, which can situationally mean: zero, one, or multiple ones.

    This is precisely what Get-ADUser does: the specific number of output objects depends on the arguments that were given; that is why the Get-AdUser help topic only mentions scalar type ADUser as the output type and states that it "returns one or more" of them.

    Generally, the PowerShell pipeline is designed to be a conduit for a stream of objects, whose count need not be known in advance, with commands typically outputting objects one by one, as soon as they become available, and with receiving commands typically also processing them one by one, as soon as they're being received. (see about_Pipelines).


    It is the PowerShell engine itself that automatically collects multiple outputs for you in an [object[]] array[2] if needed, notably if you capture output via a variable assignment or use a command call via (...), the grouping operator (or $(...), the subexpression operator[3], or @(...), the array subexpression operator, discussed in detail below), as an expression:

    # Get-ChildItem C:\Windows has *multiple* outputs, so PowerShell
    # collects them in an [object[]] array.
    PS> $var = Get-ChildItem C:\Windows; $var.GetType().Name
    Object[]
    
    # Ditto with (...) (and also with $(...) and always with @(...))
    PS> (Get-ChildItem C:\Windows).GetType().Name
    Object[]
    

    However, if a given command - possibly situationally - only outputs a single object, you'll then get just that object itself - it is not wrapped in an array (unless you use @(...) - see below):

    # Get-Item C:\ (always) returns just 1 object.
    PS> $var = Get-Item C:\; $var.GetType().Name
    DirectoryInfo # *not* a single-element array, 
                  # just the System.IO.DirectoryInfo instance itself
    

    What can get tricky is that a given command can situationally produce either one or multiple outputs, depending on inputs and runtime conditions, so the engine may return either a single object or an array.

    # !! What $var receives depends on the count of subdirs. in $HOME\Projects:
    PS> $var = Get-ChildItem -Directory $HOME\Documents; $var.GetType().Name
    ??? # If there are *2 or more* subdirs: an Object[] array of DirectoryInfos.
        # If there is only *one* subdir.: a DirectoryInfo instance itself.
        # (See below for the case when there is *no* output.)
    

    @(...), the array-subexpression operator, is designed to eliminate this ambiguity, if needed: By wrapping a command in @(...), PowerShell ensures that its output is always collected as [object[]] - even if the command happens to produce just one output object or even none:

    PS> $var = @(Get-ChildItem -Directory $HOME\Projects); $var.GetType().Name
    Object[] # Thanks to @(), the output is now *always* an [object[]] array.
    

    With variable assignments, a potentially more efficient alternative is to use an [array] type constraint to ensure that the output becomes an array:

    # Alternative to @(...)
    # Note: You may also create a strongly typed array, with on-demand type conversions:
    #       [string[]] $var = ...
    PS> [array] $var = Get-ChildItem -Directory $HOME\Documents; $var.GetType().Name
    Object[]
    

    Note:


    Taking a step back: Ensuring that collected output is an array is often not necessary, due to PowerShell's unified handling of scalars and collections in v3+:


    If a command produces no output, you'll get "nothing" (strictly speaking: the [System.Management.Automation.Internal.AutomationNull]::Value singleton), which in most cases behaves like $null[4]:

    # Get-Item nomatchingfiles* produces *no* output.
    PS> $null -eq (Get-Item nomatchingfiles*)
    True
    
    # Conveniently, PowerShell lets you call .Count on this value, which the
    # behaves like an empty collection and indicates 0.
    PS> (Get-Item nomatchingfiles*).Count
    0
    

    [1] It is possible to output entire collections as a whole to the pipeline (in PowerShell code with Write-Output -NoEnumerate $collection or, more succinctly, , $collection), but that is then just another object in the pipeline that happens to be a collection itself. Outputting collections as a whole is an anomaly, however, that changes how commands you pipe to see the output, which can be unexpected; a prominent example is ConvertFrom-Jsons unexpected behavior prior to v7.0.

    [2] a System.Array instance whose elements are of type System.Object, allowing you to mix objects of different types in a single array.

    [3] Use of (...) is usually sufficient; $(...) is only needed for string interpolation (expandable strings) and for embedding whole statements or multiple commands in a larger expression; note that $(...), unlike (...) by itself, unwraps single-element arrays; compare (, 1).GetType().Name to $(, 1).GetType().Name; see this answer.

    [4] There are scenarios in which "nothing" behaves differently from $null, notably in the pipeline and in switch statements, as detailed in this comment on GitHub. GitHub issue #13465 is a green-lit enhancement to make "nothing" more easily distinguishable from $null, by supporting -is [System.Management.Automation.AutomationNull] as a test; however, as of PowerShell (Core) 7.3.8 no one has stepped up to implement it yet.