Unexected results in Receive-Job

2020-03-26 07:56发布

问题:

I noticed weird behavior when using jobs in PowerShell 5.0. Running job that returns PSObject, also returns some hashtable. Jobs that return strings, integers, etc work propertly.

Running

Start-Job { New-Object PSObject -Property @{ A = 1 } } |
  Receive-Job -Wait -AutoRemoveJob

returns

A                     : 1
RunspaceId            : 6921e85f-301e-4e95-8e4b-c0882fc2085f
PSSourceJobInstanceId : 2992ef77-5642-4eac-8617-f26449a87801

Running

Start-Job { New-Object PSObject } | Receive-Job -Wait -AutoRemoveJob

returns

@{PSComputerName=localhost; RunspaceId=3e783d5f-254a-4951-bb4a-7ff6fa2812c5; PSShowComputerName=False; PSSourceJobInstanceId=1d951dec-0823-4cde-8219-4a6263550441}

However, running

Start-Job { ,@(New-Object PSObject -Property @{ A = 1 }) } |
  Receive-Job -Wait -AutoRemoveJob

returns

A
-
1

Why does Receive-Job cmdlet add that hashtable only for PSObjects?

UPDATE: Same in PowerShell 4.0.

回答1:

PowerShell is not a WYSIWYG shell. Usually, "what you get" is not a text, but objects with properties and methods. And "what you see" is some text representation of them. In many cases by default PowerShell does not display all of the object's properties, but only most common ones, as defined in format files. And some objects use custom formatting, like strings and integers only display their value, but not any of its properties, and collections display their content but not collections themselves.

So, in fact, PowerShell adds extra properties to all objects received from job. But this properties not always get displayed. You can see that extra properties by passing job output to Get-Member cmdlet:

Start-Job { 1,'',@() } | Receive-Job -Wait -AutoRemoveJob | Get-Member

or to formatting cmdlet with appropriate options to force formatting primitive types and to not enumerate collection:

Start-Job { 1,'',@() } | Receive-Job -Wait -AutoRemoveJob | Format-List -Force -Expand CoreOnly


回答2:

To complement PetSerAl's excellent answer with a focus on the hashtable that wasn't (a hashtable), based on PetSerAl's helpful comments:

The output from Start-Job { New-Object PSObject } | Receive-Job -Wait -AutoRemoveJob,

@{PSComputerName=localhost; RunspaceId=3e783d5f-254a-4951-bb4a-7ff6fa2812c5; PSShowComputerName=False; PSSourceJobInstanceId=1d951dec-0823-4cde-8219-4a6263550441}

only looks like a hashtable literal; in fact, it is the default output formatting for "property-less" custom objects that, in fact, do have properties, but only ones added by PowerShell itself.

This representation is suspiciously similar to a hashtable literal, but the quoting around values that would normally need it is missing - such as around localhost.
Also note that outputting an actual hashtable results in a much nicer, two-column key-value format.

Note that PS still considers a custom object that originally had no properties property-less even after PS itself has added properties to it, such as by Receive-Job here - see below for details.

In its original state (no properties added by PS yet), a property-less object's default output is empty (the empty string). (Try New-Object PSCustomObject directly at the prompt.)

Once Receive-Job has added its "meta" properties to the custom object, their existence triggers the hashtable-like output formatting.


PetSerAl has provided a link to the source code, which suggests that the "PropertyLessObject" formatting is triggered under the following conditions:

  • An object has no properties at all or only has properties automatically added by PowerShell in the context of remoting (which apparently also includes job-related cmdlets), as done by Receive-Object here.

  • To put it differently: Properties automatically added by PS aren't taken into account when deciding whether an object is propertyless.

The source-code link will tell you the specific 3-, 4-, or 5-element sets of remoting properties that may be added during remoting and trigger the formatting, but here's a minimal (3-property) example.
Again, note that the hashtable-like formatting is only triggered because the only properties that the object has are named for remoting-related, automatically added properties:

PS> [PSCustomObject] @{PSComputerName='Hi, Mom'; RunspaceId=0; PSShowComputerName=$true}
@{PSComputerName=Hi, Mom; RunspaceId=0; PSShowComputerName=False}

Note that even though a hashtable literal is involved in the command, it is merely used to construct a custom object.

You can force a normal list or table view with Format-List -Force or Format-Table -Force, but note that Boolean property PSShowComputerName never shows up and instead implicitly controls whether the associated PSComputerName property is included in the list / table.


PetSerAl also points out that you can get the hashtable-like output format on demand for any custom object: simply invoke .PSObject.ToString() (note the crucial .PSObject part; without it, you get empty output).

PS> ([pscustomobject] @{ one = 'Hi, Mom'; two = 2 }).PSObject.ToString()
@{one=Hi, Mom; two=2}

Or, more simply, with string interpolation (which, presumably, simply calls .PSObject.ToString() behind the scenes):

PS> "$([pscustomobject] @{ one = 'Hi, Mom'; two = 2 })"
@{one=Hi, Mom; two=2}

Note that this kind of string interpolation does not work for instances of any other types (objects not of type [System.Management.Automation.PSCustomObject]):

  • PowerShell defers to their .ToString() method even when you invoke .PSObject.ToString().

  • A directly .NET-based type (e.g., as added with Add-Type) by default simply returns its full type name (both from .ToString() / .PSObject.ToString() and as default output in PS); e.g.:

    PS> (Add-Type -PassThru 'namespace net.same2u { public class SomeType {} }')::New()
    net.same2u.SomeType  # the full type name
    
  • The same applies to instances of custom PowerShell classes (defined with class { ... }).