Reputation: 1469
If a cmdlet returns array of array, for example:
function test() {
$results = New-Object System.Collections.ArrayList
$array = @()
for ($idx = 0; $idx -lt 3; $idx++) {
$obj = New-Object PSObject -Property @{
"key1" = "value1";
}
$array += @($obj)
}
[Void] $results.add($array)
return ,$results.TOArray()
}
Then the output is different when the return value is assigned.
if run test
directly it displays:
test
Length : 3
LongLength : 3
Rank : 1
SyncRoot : {@{key1=value1}, @{key1=value1}, @{key1=value1}}
IsReadOnly : False
IsFixedSize : True
IsSynchronized : False
Count : 3
while assign to a variable:
$result = test
$result
key1
----
value1
value1
value1
And if a cmdlet returns a one level array, the output of test
and $(test)
is the same.
function test() {
$array = New-Object System.Collections.ArrayList
for ($idx = 0; $idx -lt 3; $idx++) {
$obj = New-Object PSObject -Property @{
"key1" = "value1";
}
$array += @($obj)
}
return ,$array
}
test
's output:
key1
----
value1
value1
value1
Upvotes: 2
Views: 1324
Reputation: 437062
PetSerAl has provided the crucial pointer in a terse comment:
The difference in output rendering comes down to the fact that:
test
is a command (a call to a function, cmdlet, or external program)$result
(which previously captured test
's output) is an expression (something involving only variable references, PowerShell's operators and .NET method calls, outside a pipeline - though it may contain nested commands).By outputting , $results.ToArray()
from your test
function (a function is one form of a command), you're using ,
, the array-construction operator, to wrap $results.ToArray()
(which results in array of arrays) in an auxiliary, transitory single-element array, which is a common technique to ensure that a collection is passed on as a single object rather than having its elements enumerated.
That is, the aux. wrapper array is:
invariably lost on output to the pipeline, due to the pipeline's automatic unwrapping (unrolling) behavior,
but it ensures that the wrapped array is seen as a single object by the next command in the pipeline.
The conceptually clearer, but more verbose equivalent of , $results.ToArray()
inside your function is Write-Output -NoEnumerate $results.ToArray()
; that is, PowerShell's usually implicit output is made explicit, with a request to suppress the default behavior of enumerating output collections.
Given that there is no additional command in the pipeline, tests
output is implicitly printed to the screen.
In the case at hand, printing an array of arrays as a single object results in the property-list output formatting you saw.
By contrast, $result
, due to being an expression is implicitly enumerated. That is, the array of arrays captured from test
- without the aux. wrapper array! - is sent one element at a time to the output-formatting system, and these elements then render more meaningfully.
To provide a simpler example:
Assume your function test
uses return , , (1..3)
to output a container array containing a 3-element array that is finally wrapped in an aux. single-element array (as an aside: return
in PowerShell is just syntactic sugar for exiting a function or script block, it has no direct relationship with what is output).
Executing the test
function is then the equivalent of direct execution of the following expression:
, , (1..3)
That is, the outer, aux. array is again discarded due to implicit enumeration and , (1..3)
is rendered as a single object, resulting in the property-list format:
Length : 3
LongLength : 3
Rank : 1
SyncRoot : {1, 2, 3}
IsReadOnly : False
IsFixedSize : True
IsSynchronized : False
Count : 3
By contrast, executing $result
(after running $result = test
) is then the equivalent of just:
, (1..3)
That is, the outer, aux. array was lost during $result = test
, the container array too is now implicitly enumerated, and (1..3)
as a single object renders more meaningfully (you can't visually distinguish it from sending 1..3
directly to the pipeline, i.e., element by element):
1
2
3
When a command or expression is neither captured in a variable nor sent to the pipeline to another command nor redirected (with >
or >>
), it is implicitly printed to the screen (host), using PowerShell's default output-formatting system.
You can think of a command such as:
test
being the equivalent of[1]:
test | Out-Host
Out-Host
automatically chooses a Format-*
cmdlet to use for rendering that is suitable for the input at hand, based on the first input object:
If that object has 4 or fewer properties, Format-Table
is chosen; otherwise, it is Format-List
.
However, if that first input object is a collection (implements IEnumerable
), it is that collection's first element that the choice of formatting cmdlet is based on (as opposed to the collection type as a whole), and the collection's elements are formatted individually using that cmdlet.
In the case of your $result
variable getting output, the input array's first element is a [pscustomobject]
instance (created with New-Object PSObject
) with 1 property, key1
; therefore, Format-Table
is chosen, and the [pscustomobject]` instances that make up the array are shown in a tabular format.
By contrast, in the case of your test
call, the input array's first element is another array, which is itself not further enumerated. Get-Member -InputObject (1,2) -Type Property
reveals that an array has 8 properties (Count
, IsFixedSize
, IsReadOnly
, IsSynchronized
, Length
, LongLength
, Rank
, SyncRoot
), which is why Format-List
is chosen, listing each property as a name / value pair on its own line.
Of course, you can opt to use a formatting cmdlet explicitly, and PetSerAl points out that the formatting cmdlets support the -Expand
parameter, which gives you control over how input objects that are collections are formatted: you can ask to have the collection enumerated, i.e., to print its elements (-Expand EnumOnly
, which is the default), to show just the collection's own properties, without printing its elements (-Expand CoreOnly
), or both (-Expand Both
).
Note, however, that you cannot request additional levels of enumeration via -Expand
, so your test
output cannot be directly formatted to show the individual elements of your nested array.
However, it is trivial to make that happen by piping to Write-Output
, which performs the additional level of enumeration required to render the elements individually:
test | Write-Output
[1] More accurately, as PetSerAl points out, it is: . { test } 2>&1 | Out-Default
, enabling users to override the Out-Default
cmdlet for custom formatting.
Upvotes: 1