Search code examples
powershellreturnstdoutreturn-value

Powershell function returns stdout? (Old title: Powershell append to array of arrays appends stdout?)


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.


Solution

    • PowerShell doesn't have return values, it has a success output stream (the analog of stdout in traditional shells).

      • The PowerShell pipeline serves as the conduit for this stream, both when capturing command output in a variable and when sending it to another command via |, the pipeline operator
    • Any 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.

      • Output is sent instantly to the success output stream, as it is being produced - even before the script or function exits.
    • 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_Redirection 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

        • A conceptually clearer (but less efficient) alternative is to use Write-Output -NoEnumerate
        • See this answer for more information.

    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.

    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.