-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Description
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
$_/$PSItemor 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] }- Perhaps not surprisingly, if you cast / type-constrain with
[psobject]- for which there is no need, and in most scenarios this will have no practical implications; more surprisingly, however, the same applies to[pscustomobject], given that these type accelerators are effectively the same and both refer to[System.Management.Automation.PSObject](see Why is [pscustomobject] the same as [psobject], even though a distinct [System.Management.Automation.PSCustomObject] type exists? #4344).
# -> $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):
-
In Windows PowerShell,
ConvertTo-Jsonis affected, as originally reported
in ConvertFrom-Json doesn't unwrap arrays on output, resulting in an extraneous wrapper JSON object withCountandValueproperties on reconversion #3153 - see below.
(PowerShell Core is no longer affected, because that issue has been fixed
via Remove System.Array type data #3231) -
The -f operator doesn't recognize a [psobject]-wrapped array #14355
-
A "hybrid" bareword command argument that is parsed as a number unexpectedly behaves like a string with -f (format operator) #17199 [declared to be by design]
-
Array.Sort method call situationally ignores string-comparer argument #14829
-
Invoke-Command -SSHConnection breaks if the HostName string is [psobject]-wrapped #10687 [fixed in v7.0.0-preview.5]
-
Conversion of [datetime] property in class does not work #20401 (comment)
-
Write-Host renders
IDictionaryinconsistently depending on if the value is piped in #24671 -
Start-Transcriptthrows when$Transcriptis aPSObjectwrappedstring#24740 -
ConvertTo-Json does not convert additional members of nested properties #19185 (comment)
-
$_in aForEach-Objectscript block reflects each input object[psobject]-wrapped, causing-is [pscustomobject]tests to become meaningless, as then any input object passes the test - see this SO question -
Passing an incidentally wrapped
[psobject]-wrapped instance to an external serialization API such as[Newtonsoft.Json.JsonConvert]::SerializeObject()causes unexpected behavior - see this Stack Overflow post. -
For the sake of completeness: bugs that still affect Windows PowerShell, but have since been fixed:
-
An
[xml]-related bug: see this Stack Overflow post. -
The
$Transcriptpreference variable doesn't recognize a[psobject]-wrapped string (such as output byJoin-Path) as a string: see this Stack Overflow post.
-
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