PowerShell output is crossing between functions

Solution 1:

tl; dr:

Force synchronous output to the console with Out-Host:

getUsersAndGroups | Out-Host
getRunningProcesses | Out-Host

Note: You can alternatively use one of the Format-* cmdlets, which also forces synchronous output; e.g., getUsersAndGroups | Format-Table.

Inside a PowerShell session:

  • This is primarily a display problem, and you do not need this workaround for capturing output in a variable, redirecting it to a file, or passing it on through the pipeline.

  • Caveat: Sending to Out-Host means that the commands' output can no longer be captured or redirected; see this answer for a - suboptimal - workaround.

From the outside, when calling the PowerShell CLI (powershell -file ... or powershell -command ...):

  • Actual data loss may occur if Out-Host is not used, because pending asynchronous output may never get to print if the script / command ends with exit; e.g.:

    # !! Prints only 'first'
    powershell.exe -command "'first'; [pscustomobject] @{ foo = 'bar' }; exit"
    
  • However, unlike in intra-PowerShell-session use, Out-Host fixes both the display and the data-capturing / redirection problem, because Out-Host's output too is sent to stdout, as seen by an outside caller (but note that the for-display representations that PowerShell's output-formatting system produces aren't generally suitable for programmatic processing).

Note: All of the above also applies to PowerShell (Core) 7+ and its pwsh CLI, up to at least v7.2.


The explanation of PowerShell's problematic behavior in this case:

It may helpful to demonstrate the problem with an MCVE (Minimal, Complete, and Verifiable Example):

Write-Host "-- before"
[pscustomobject] @{ one = 1; two = 2; three = 3 }
Write-Host "-- after"

In PSv5+, this yields:

-- before

-- after
one two three
--- --- -----
  1   2     3

What happened?

  • The Write-Host calls produced output synchronously.

    • It is worth noting that Write-Host bypasses the normal success output stream and (in effect) writes directly to the console - mostly, even though there are legitimate uses, Write-Host should be avoided.

    • However, note that even output objects sent to the success output stream can be displayed synchronously, and frequently are, notably objects that are instances of primitive .NET types, such as strings and numbers, as well as objects whose implicit output formatting results in non-tabular output as well as types that have explicit formatting data associated with them (see below).

  • The implicit output - from not capturing the output from statement [pscustomobject] @{ one = 1; two = 2; three = 3 } - was unexpectedly not synchronous:

    • A blank line was initially produced.
    • All actual output followed the final Write-Host call.

This helpful answer explains why that happens; in short:

  • Implicit output is formatted based on the type of objects being output; in the case at hand, Format-Table is implicitly used.

  • In Psv5+, implicitly applied Format-Table now waits up to 300 msecs. in order to determine suitable column widths.

    • Note, however, that this only applies to output objects for whose type table-formatting instructions are not predefined; if they are, they determine the column widths ahead of time, and no waiting occurs.

    • To test whether a given type with full name <FullTypeName> has table-formatting data associated with it, you can use the following command:

          # Outputs $true, if <FullTypeName> has predefined table-formatting data.
          Get-FormatData <FullTypeName> -PowerShellVersion $PSVersionTable.PSVersion |
            Where-Object { 
              $_.FormatViewDefinition.Control.ForEach('GetType') -contains [System.Management.Automation.TableControl] 
            }
      
  • Unfortunately, that means that subsequent commands execute inside that time window and may produce unrelated output (via pipeline-bypassing output commands such as Write-Host) or prompt for user input before Format-Table output starts.

    • When the PowerShell CLI is called from the outside and exit is called inside the time window, all pending output - including subsequent synchronous output - is effectively discarded.

The problematic behavior is discussed in GitHub issue #4594; while there's still hope for a solution, there has been no activity in a long time.


Note: This answer originally incorrectly "blamed" the PSv5+ 300 msec. delay for potentially surprising standard output formatting behavior (namely that the first object sent to a pipeline determines the display format for all objects in the pipeline, if table formatting is applied - see this answer).