I've learnt from this thread how to append to an array of arrays. However, I've observed the weirdest behaviour ever. It seems to append stdout too! Suppose I want to append to an array of arrays, but I want to echo debug messages in the loop. Consider the following function.
function WTF {
$Result = @()
$A = 1,2
$B = 11,22
$A,$B | % {
Write-Output "Appending..."
$Result += , $_
}
return $Result
}
Now if you do $Thing = WTF
, you might think you get a $Thing
that is an array of two arrays: $(1,2)
and $(11,22)
. But that's not the case. If you type $Thing
in the terminal, you actually get:
Appending...
Appending...
1
2
11
22
That is, $Thing[0]
is the string "Appending...", $Thing[1]
is the string "Appending...", $Thing[2]
is the array @(1,2)
, and $Thing[3]
is the array @(11,22)
. Nevermind that they seem to be in a weird order, which is a whole other can of worms I don't want to get into, but why does the echoed string "Appending..." get appended to the result??? This is so extremely weird. How do I stop it from doing that?
EDIT
Oh wait, upon further experimenting, the following simpler function might be more revealing:
function WTF {
$Result = @()
1,2 | % {
Write-Output "LOL"
}
return $Result
Now if you do $Thing = WTF
, then $Thing
becomes an array of length 2, containing the string "LOL" twice. Clearly there is something fundamental about Powershell loops and or Write-Output
that I'm not understanding.
ANOTHER EDIT!!!
In fact, the following even simpler function does the same thing:
function WTF {
1,2 | % {
Write-Output "LOL"
}
}
Maybe I just shouldn't be using Write-Output
, but should use Write-Information
or Write-Debug
instead.
PowerShell doesn't have return values, it has a success output stream (the analog of stdout in traditional shells).
|
, the pipeline operatorAny statement - including multiple ones, possibly in a loop - inside a function or script can write to that stream, typically implicitly - by not capturing, suppressing, or redirecting output - or explicitly, with Write-Output
, although its use is rarely needed - see this answer for more information.
return
exists for flow control, independently of PowerShell's output behavior; as syntactic sugar you may also use it to write to the output stream; that is, return $Result
is syntactic sugar for: $Result; return
, with $Result
by itself producing implicit output, and return
exiting the scope.
To avoid polluting the success output stream - intended for data output only - with status messages, write to one of the other available output streams - see the conceptual about_Output_Streams help topic.
Write-Verbose
is a good choice, because it is silent by default, and can be activated on demand, either via $VerbosePreference
= 'Continue'
, or, on a per-call basis, with the common -Verbose
parameter, assuming the script or function is an advanced one.
Write-Host
, by contrast, unconditionally prints information to the host (display), and allows control over formatting, notably coloring.
Outputting a collection (array) from a script or function enumerates it. That is, instead of sending the collection itself, as a whole, its elements are sent to PowerShell's pipeline, one by one, a process called streaming.
PowerShell commands generally expect streaming output, and may not behave as expected if you output a collection as a whole.
When you do want to output a collection as a whole (which may sometimes be necessary for performance reasons), wrap them in a transient aux. single-element array, using the unary form of ,
, the array constructor operator: , $Result
Write-Output -NoEnumerate
Therefore, the PowerShell idiomatic reformulation of your function is:
function WTF {
# Even though no parameters are declared,
# these two lines are needed to activate support for the -Verbose switch,
# which implicitly make the function an *advanced* one.
# Even without it, you could still control verbose output via
# the $VerbosePreference preference variable.
[CmdletBinding()]
param()
$A = 1,2
$B = 11,22
# Use Write-Verbose for status information.
# It is silent by default, but if you pass -Verbose
# on invocation or set $VerbosePreference = 'Continue', you'll
# see the message.
Write-Verbose 'Appending...'
# Construct an array containing the input arrays as elements
# and output it, which *enumerates* it, meaning that each
# input array is output by itself, as a whole.
# If you need to output the nested array *as a whole*, use
# , ($A, $B)
$A, $B
}
Sample invocation:
PS> $nestedArray = WTF -Verbose
VERBOSE: Appending...
Note:
Only the success output (stream 1
) was captured in variable $nestedArray
, whereas the verbose output (stream 4
) was passsed through to the display.
$nestedArray
ends up as an array - even though $A
and $B
were in effect streamed separately - because PowerShell automatically collects multiple streamed objects in an array, which is always of type [object[]]
.
A notable pitfall is that if there's only one output object, it is assigned as-is, not wrapped in an array.
To ensure that a command's output is is always an array, even in the case of single-object output:
You can enclose the command in @(...)
, the array-subexpression operator
$txtFiles = @(Get-ChildItem *.txt)
In the case of a variable assignment, you can also use a type constraint with [array]
(effectively the same as [object[]]
):
[array] $txtFiles = Get-ChildItem *.txt
However, note that if a given single output object itself is a collection (which, as discussed, is unusual), no extra array wrapper is created by the commands above, and if that collection is of a type other than an array, it will be converted to a regular [object[]]
array.
Additionally, if @(...)
is applied to a strongly typed array (e.g., [int[]] (1, 2)
, it is in effect enumerated and rebuilt as an [object[]]
array; by contrast, the [array]
type constraint (cast) preserves such an array as-is.
As for your specific observations and questions:
I've learnt from this thread how to append to an array of arrays
While using +=
in order to incrementally build an array is convenient, it is also inefficient, because a new array must be constructed every time - see this answer for how to construct arrays efficiently, and this answer for how to use an efficiently extensible list type as an alternative.
Nevermind that they seem to be in a weird order, which is a whole other can of worms I don't want to get into
Output to the pipeline - whether implicit or explicit with Write-Output
is instantly sent to the success output stream, notably while a script or function is still running.
Thus, your Write-Output
output came first, given that you didn't output the $Result
array until later, via return
.
How do I stop it from doing that?
As discussed, don't use Write-Output
for status messages.