Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Objects are situationally invisibly [psobject]-wrapped, sometimes causing unexpected behavior. #5579

Copy link
Copy link
@mklement0

Description

@mklement0
Issue body actions

There are (at least?) 5 basic scenarios in which objects / properties end up invisibly [psobject]-wrapped, which can lead to subtle differences in behavior:

Note: By [psobject]-wrapped I mean an object for which -is [psobject] returns $true.

  • Objects received via the pipeline when processed via $_ / $PSItem or an [object] or untyped parameter - but not if passed as an argument:
###  Via the *pipeline*:
# -> $true: use of $_
'foo' | ForEach-Object { $_ -is [psobject] }
# -> $true: use of [object]-typed parameter (or untyped)
'foo' | & { param([parameter(valuefrompipeline)] [object] $foo) process { $foo -is [psobject] } }

# -> $false: use of a specifically typed parameter
'foo' | & { param([parameter(valuefrompipeline)] [string] $foo) process { $foo -is [psobject] } }

### As an *argument*:
# -> $false: untyped parameter (or [object]-typed), or any type other than [psobject]
& { param($p) $p -is [psobject] } 'foo'
& { param([string] $p) $p -is [psobject] } 'foo'
# -> $true: only with explicit [psobject]-typing:
& { param([psobject] $p) $p -is [psobject] } 'foo'
  • Objects output by a - binary - cmdlet - but not objects output by a PowerShell function or script or objects returned from an expression. (Note that the arrays that PowerShell implicitly constructs for collecting multiple output objects on assignment to a variable or when a command call participates in an expression are themselves not [psobject]-wrapped.)
# -> $true: output from binary *cmdlet*
(Write-Output 'foo') -is [psobject]

# -> $false: output from *PowerShell code*
(& { 'foo' }) -is [psobject]

# -> $false: output from an expression
'foo' -is [psobject]
  • The value of any calculated property whose value is determined via a script block - but not if the value is determined by a property name (string):
# -> $true: property value is determined by *script block*
(Get-Item / | Select-Object @{ l='foo'; e={ $_.Name } }).foo -is [psobject]

# -> $false: property value is determined by *name* (string)
(Get-Item / | Select-Object @{ l='foo'; e='Name' }).foo -is [psobject]
  • The elements of the collections returned by the intrinsic .ForEach() and .Where() methods are always [psobject]-wrapped (implied by the output collection type, System.Collections.ObjectModel.Collection<PSObject>):
# -> $true, $true
(1..2).ForEach({ $_ }) | ForEach-Object { $_ -is [psobject] }
# -> $true
([psobject] 42) -is [psobject]

# -> $true: [pscustomobject] is the same as [psobject] and 
#   does *not* create a custom object from arbitrary operands
# (only [pscustomobject] @{ ... } works)
([pscustomobject] 42) -is [psobject]

As for real-world ramifications (in addition to the difference in Get-Member representation - see below):

Note that even non-wrapped instances in the sense above do have ETS-defined properties (e.g., [datetime]::now.psextended reveals the .DateTime property).

(While #4347 may sound related, the distinct issue there is that additional properties may be added to outputs by provider cmdlets.)

Examples:

# Two seemingly equivalent ways of constructing a [string] instance:
# Using an expression vs. using a command:
$o1 = 'hi'; $o2 = New-Object System.String 'hi'

# Only the New-Object (*command*-generated) instance is wrapped, however.
> ($o1 -is [psobject]), ($o2 -is [psobject])
False
True

# With *multiple outputs*, *only the individual elements are wrapped*,
# because the implicitly constructed array that collects the output is itself
# NOT wrapped.
> $arr = Write-Output 1, 2; $arr -is [psobject]; $arr[0] -is [psobject]
False
True

# Two ways of constructing a [pscustomobject] instance
# with an array-valued .foo property that is *not* wrapped
# ($o2 itself, by contrast, *is* wrapped, because it is constructed with
# a *conmand*).
$o1 = [pscustomobject] @{ foo = 1, 2 }
$o2 = New-Object pscustomobject -property @{ 'foo' = 1, 2  }

# *Seemingly* equivalent ways, which, however result in a [psobject]-wrapped
# .foo property.
# Calculated property:
$o3 = '' | Select-Object @{ l='foo'; e = { 1, 2 } }
# Explicit [psobject] cast:
$o4 = [pscustomobject] @{ foo = [psobject] (1, 2) }

# All are instances of [System.Management.Automation.PSCustomObject]
> $o1, $o2, $o3, $o4 | % GetType | % Name
PSCustomObject
PSCustomObject
PSCustomObject
PSCustomObject

# All .foo properties are instances of [System.Object[]]
> $o1.foo, $o2.foo, $o3.foo, $o4.foo | % GetType | % Name
Object[]
Object[]
Object[]
Object[]

# However, only $o3 and $o4's .foo properties are considered [psobject] instances:
> ($o1.foo -is [psobject]), ($o2.foo -is [psobject]), ($o3.foo -is [psobject]), ($o4.foo -is [psobject])
False
False
True
True

# This subtle distinction can result in different behavior.
# Note how Get-Member represents the properties differently.
> $o1, $o3 | Get-Member foo | % Definition
Object[] foo=System.Object[]
System.Object[] foo=1 2

# *Windows PowerShell* only:
# Note how the JSONification of $o3 has an extraneous "value" wrapper for
# the array and a "Count" property.
> $o1, $o3 | ConvertTo-Json
[
  {
      "foo":  [
                  1,
                  2
              ]
  },
  {
      "foo":  {
                  "value":  [
                                1,
                                2
                            ],
                  "Count":  2
              }
  }
]

Environment data

Current as of:

PowerShell Core 7.5.0-rc.1
Windows PowerShell 5.1
Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Discussionthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassifthe issue may not have a clear classification yet. The issue may generate an RFC or may be reclassifWG-Enginecore PowerShell engine, interpreter, and runtimecore PowerShell engine, interpreter, and runtime

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.