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

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:

  • This is potentially more efficient in that if the RHS already happens to be an array, it is assigned as-is, whereas @(...) actually enumerates the output from ... and then reassembles the elements into a new ([object[]]) array.

    • [array] preserves the specific type of an input array by simply passing it through (e.g., [array] $var = [int[]] (1..3) stores the [int[]] array as-is in $var).
  • Placing the [array] “cast” to the left of $var = ... – which is what it makes it a type constraint on the variable – means that the type of the variable is locked in, and assigning different values to $var later will continue to convert the RHS value to [array] ([object[]]), if needed (unless you assign $null or “nothing” (see below)).


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+:

  • PowerShell exposes intrinsic members even on scalar (non-collection) objects that allow you treat them like collections.

    • E.g. (42).Count is 1 ((42).Length works too); it is treated like an integer collection with 1 element, and this also applies to indexing: (42)[0] is 42, i.e. the virtual collection’s first and only element.
    • Caveat: Type-native members take precedence, which is a pitfall with strings: 'foo'.Length is 3, the string’s length – but 'foo'.Count works (is 1); however, 'foo'[0] is invariably 'f', because the [string] type’s native indexer returns the individual characters in the array (workaround: @('foo')[0])
  • See this answer for details.


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; the linked issue is a feature request to make “nothing” more easily distinguishable from $null, by supporting -is [AutomationNull] as
a test.

Leave a Comment