25

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])

output:

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           1.1.0.1

Cheers,

M

2 Answers 2

30

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 (without a cmdlet call) falls into the former category, using Write-Output into the latter.

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

# 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:

Sign up to request clarification or add additional context in comments.

5 Comments

Amazing - thanks for the comprehensive answer. I was semi-aware of PowerShell's wrappers but this explains the 'oddness' I was seeing. In my case I've changed $x -is [pscustomobject] to $x -is [System.Management.Automation.PSCustomObject] and it's behaving how I expected it to now.
This explanation is 1k points worthy. I was struggling on why Write-Host (std output) was being taken as a function return. So now its clear the two alternatives for writing functions: Implicit vs cmd-let output. Amazing!
I'm glad to hear the answer is helpful, @LeonardoT; perhaps it was just a typo, but note that the distinction is between implicit expression output on the one hand and Write-Output on the other, not Write-Host - the latter bypasses the success output stream (the PowerShell equivalent of stdout) and writes directly to the host (console) - see this answer. The short of it is that explicit use of Write-Output is rarely necessary - implicit output is not only more concise but also faster.
An other difference is that return ends the function: function Invoke-X{ write-output "output"; return "return" } (returns array with output and return) vs function Invoke-Y{ return "return"; write-output "output" } (returns the string return)
@sschoof Yes, return is a flow-control statement that unconditionally exits the enclosing function or script block or script - see about_Return. return <some-command-or-expression> is just syntactic sugar for <some-command-or-expression>; return.
3

Also note that adding Write-Output debug messages, unlike .net, will change the return type to an array. Adding write lines can break functions.

function Invoke-X {
    $o1 = [pscustomobject] @{ foo = 1, 2 }
    return $o1
}

function Invoke-Y {

    $o1 = [pscustomobject] @{ foo = 1, 2 }
    Write-Output "Debug messageY"
    return $o1
 }

function Invoke-Z {
    $o1 = [pscustomobject] @{ foo = 1, 2 }
    Write-Output "Debug messageZ"
    return ,$o1
 }

$x = Invoke-X;
$y = Invoke-Y;
$z = Invoke-Z;

Write-Host
Write-Host "X  Type: " $x.GetType().FullName $x.foo
Write-Host
Write-Host "Y  Type: " $y.GetType().FullName
Write-Host "Y0 Type: " $y[0].GetType().FullName $y[0]
Write-Host "Y1 Type: " $y[1].GetType().FullName $y[1].foo
Write-Host
Write-Host "Z  Type: " $z.GetType().FullName
Write-Host "Z0 Type: " $z[0].GetType().FullName $z[0]
Write-Host "Z1 Type: " $z[1].GetType().FullName $z[1].foo

Gives:

X  Type:  System.Management.Automation.PSCustomObject 1 2
Y  Type:  System.Object[]
Y0 Type:  System.String Debug messageY
Y1 Type:  System.Management.Automation.PSCustomObject 1 2
Z  Type:  System.Object[]
Z0 Type:  System.String Debug messageZ
Z1 Type:  System.Management.Automation.PSCustomObject 1 2

1 Comment

If you want to write debug messages you’re better off using Write-Debug or Write-Host, whereas by design Write-Output sends values to the pipeline (see learn.microsoft.com/en-us/powershell/module/…) which effectively means it becomes part of the return value(s) from the function.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.