PowerShell – “Write-Output” vs “return” in functions

  • A+

I've been using PowerShell for a number of years, and I thought I had a handle on some of its more 'eccentric' behaviour, but I've hit an issue I can't make head nor tail of...

I've always used "return" to return values from functions, but recently I thought I'd have a look at Write-Output as an alternative. However, PowerShell being PowerShell, I've found something that doesn't seem to make sense (to me, at least):

function Invoke-X{ write-output @{ "aaa" = "bbb" } }; function Invoke-Y{ return @{ "aaa" = "bbb" } };  $x = Invoke-X; $y = Invoke-Y;  write-host $x.GetType().FullName write-host $y.GetType().FullName  write-host ($x -is [hashtable]) write-host ($y -is [hashtable])  write-host ($x -is [pscustomobject]) write-host ($y -is [pscustomobject]) 


System.Collections.Hashtable System.Collections.Hashtable True True True False 

What is the difference between $x and $y (or 'write-output' and 'return') that means they're both hashtables, but only one of them '-is' a pscustomobject? And is there a generalised way I can determine the difference from code, other than obviously checking whether every hashtable I have in a variable is also a pscustomobject)?

My $PSVersionTable looks like this, in case this behaviour is specific to a particular version of PowerShell:

Name                           Value ----                           ----- PSVersion                      5.1.16299.492 PSEdition                      Desktop PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...} BuildVersion                   10.0.16299.492 CLRVersion                     4.0.30319.42000 WSManStackVersion              3.0 PSRemotingProtocolVersion      2.3 SerializationVersion  




return and [pscustomobject] are red herrings here, in a way.

What it comes down to is:

  • Implicit / expression output vs. cmdlet-produced output; using return falls into the former category, using Write-Output into the latter.

  • objects getting wrapped in mostly invisible [psobject] instances only in cmdlet output.

# implicit / expression output: NO [psobject] wrapper: @{ "aaa" = "bbb" } -is [psobject] # -> $False  # Cmdlet-produced output: [psobject]-wrapped (Write-Output @{ "aaa" = "bbb" }) -is [psobject]  # -> $True 

Note that - surprisingly - [pscustomobject] is the same as [psobject]: they both refer to type [System.Management.Automation.PSObject], which is the normally invisible helper type that PowerShell uses behind the scenes.
(To add to the confusion, there is a separate [System.Management.Automation.PSCustomObject] type.)

For the most part, this extra [psobject] wrapper is benign - it mostly behaves as the wrapped object would directly - but there are instances where it causes subtly different behavior (see below).

And is there a generalised way I can determine the difference from code, other than obviously checking whether every hashtable I have in a variable is also a pscustomobject

Note that a hashtable is not a PS custom object - it only appears that way for - any - [psobject]-wrapped object due to [pscustomobject] being the same as [psobject].

To detect a true PS custom object - created with [pscustomobject] @{ ... } or New-Object PSCustomObject / New-Object PSObject or produced by cmdlets such as Select-Object and Import-Csv - use:

$obj -is [System.Management.Automation.PSCustomObject] # NOT just [pscustomobject]! 

Note that using the related -as operator with a true PS custom object is broken as of Windows PowerShell v5.1 / PowerShell Core v6.1.0 - see below.

As an example of a situation where the extra [psobject] wrapper is benign, you can still test even a wrapped object for its type directly:

(Write-Output @{ "aaa" = "bbb" }) -is [hashtable]  # $True 

That is, despite the wrapper, -is still recognizes the wrapped type. Therefore, somewhat paradoxically, both -is [psobject] and -is [hashtable] return $True in this case, even though these types are unrelated.

There is no good reason for these discrepancies and they strike me as leaky abstractions (implementations): internal constructs accidentally peeking from behind the curtain.

The following GitHub issues discuss these behaviors:


:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: