Output semantics, unrolling and streams

PowerShell · Expert · 14 min

These rules explain the strangest PowerShell bugs: arrays that become scalars and return values with unexpected extra items.

Everything uncaptured is output. return $x is just "output $x and leave"; any earlier expression that produced a value, such as a .NET method returning an index (ArrayList.Add) or a cmdlet you forgot to silence, is output too, so the caller receives an array. Discard with $null = ..., [void](...) or | Out-Null (the first two are fastest).

Unrolling: when a collection is written to the pipeline, it is enumerated item by item. A function returning a one-element array therefore gives the caller a scalar, and an empty array gives nothing ($null). To return the collection itself, prefix it with the unary comma: return , $list. On the caller side, @(...) reliably turns any result into an array.

PowerShell has six streams: 1 Success (output), 2 Error, 3 Warning, 4 Verbose, 5 Debug, 6 Information. Write-Host writes to the Information stream since 5.0, which is why it can now be captured. Redirect by number: 2>&1 merges errors into output, 3> warnings.txt saves warnings, *> $null silences everything, *>&1 merges all streams into output.

$null comparisons also involve this: an empty pipeline result is a special "AutomationNull" that behaves as $null but, unlike $null, produces no iteration in foreach.

Example

function Get-Two { 'noise'; return 'value' }        # BOTH are output
(Get-Two).Count                                     # 2
function Get-One { return @(42) }
(Get-One).GetType().Name                            # Int32   (unrolled to a scalar)
function Get-Wrapped { return , @(42) }             # unary comma keeps the array
(Get-Wrapped).GetType().Name                        # Object[]
@(Get-One).Count                                    # 1       (@() forces an array)
function Get-None { return @() }
$null -eq (Get-None)                                # True
$al = [System.Collections.ArrayList]::new()
function Add-Item { $al.Add('x'); 'done' }          # Add returns an index...
Add-Item                                            # 0 / done  (leaked output)
$null = $al.Add('y')                                # discard it like this
function Get-Info { Write-Warning 'careful'; Write-Information 'fyi'; 'data' }
$out = Get-Info 3>&1 6>&1                           # merge warning + information
$out.Count                                          # 3
Get-Info *> $null                                   # silence every stream

Remember

  • Every uncaptured value is output; return only exits
  • Collections unroll: return , $arr to keep one, @() to force an array
  • Streams 1-6; redirect with 2>&1, 3>, *> $null

Watch out for

  • Calling methods like ArrayList.Add() in a function without discarding the result, so the caller gets extra numbers.

Check yourself

  1. A function ends with `return @('only')`. What type does the caller get?
  2. Which redirection silences all streams of a command?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_redirection