Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
786 views
in Technique[技术] by (71.8m points)

arrays - 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 meant to be a stream of objects whose particular length (object count) need not be known in advance, with commands in subsequent pipeline segments processing the previous segment's output objects one by one, as they're being received (see about_Pipelines).


However, it is the PowerShell engine 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]), 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:

# 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 $HOMEProjects:
PS> $var = Get-ChildItem -Directory $HOMEDocuments; $var.GetType().Name
??? # If there are *2 or more* subdirs: an Object[] array of DirectoryInfo instances.
    # 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 $HOMEProjects); $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 $HOMEDocuments; $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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...