The handy new Get-Error cmdlet implements its pretty-printing by replacing the System.Management.Automation.ErrorRecord type name in the ETS type-name array with PSExtendedError.
Unfortunately, it does this on the _originals_ in the $Error collection, resulting in inadvertent modification that makes these instances print differently with implicit output (e.g., $Error[0]) afterwards.
# Provoke a an error.
try { 1 / 0 } catch {}
$Error[0].pstypenames | Should -Be System.Management.Automation.ErrorRecord, System.Object
# Run Get-Error, which accidentally modifies .pstypenames of the most recent error.
$null = Get-Error
# Repeating the test shows the modified .pstypenames
$Error[0].pstypenames | Should -Be System.Management.Automation.ErrorRecord, System.Object
Both tests should succeed.
The 2nd test fails:
Expected @('System.Management.Automation.ErrorRecord', 'System.Object'), but got @('PSExtendedError', 'System.Object').
That is, $Error[0]'s .pstypename property was accidentally modified.
PowerShell Core 7.0.0-preview.5
This is an unfortunate workaround to how OutOfBand formatting works. Exceptions and ErrorRecords are OutOfBand and take precedence for formatting. This is why you have to use -force with format-list to see the entire contents, but it's also how exceptions/errorrecords can show up in the middle of pipeline output formatting (well, for non-terminating ones). Not sure how to fix this without this side effect since there isn't a generic way to copy a .NET object.
@SteveL-MSFT since this modification to PSTypeNames only needs to go as deep as the ETS, I think you can just call PSObject.Copy() to clone the wrapper and modify that instead of the original PSObject stored in $error.
@vexx32 tried that, didn't work. PSObject.Copy() relies on the base object to implement ICloneable. Note that I was aware of this side effect which is why the formatter puts back the original types. However, if you assign the output to $null, the formatter doesn't even get exercised.
馃 Can you then do var newWrapper = PSObject.AsPSObject(errorRecordPSObject.BaseObject); perhaps? Bit of a run-around I'm sure but hey 馃榿
@vexx32 not sure how that buys us anything? It'll still be a ref to the original
@SteveL-MSFT, note that problem also occurs if you don't assign the output to $null:
try { 1/0 } catch {}; $e = $Error[0]; Get-Error; '----'; $e.pstypenames
An additional problem is that PSExtendedError is apparently invariably inserted _every time_ Get-Error processes a given record:
try { 1/0 } catch {}; $e = $Error[0]; Get-Error; Get-Error; '----'; $e.pstypenames
Note how the output at the end now has _two_ PSExtendedError lines.
@mklement0 duplicate typenames should already be fixed in master (coming in Preview.6). I don't think we can solve the other problem unless we rewrite out OutOfBand formatting works
There is a solution, @SteveL-MSFT, by harnessing obscure behavior in Select-Object -ExpandProperty:
Here's a PoC in PowerShell:
try { 1/0 } catch {}; $e = $Error[0]; $eClone = [pscustomobject] @{ e = $e } | select -ExpandProperty e; $null = Get-Error; $eClone.pstypenames
Note how $eClone retained its separate .pstypenames value.
You were on the right track, @vexx32, all that was missing was true:
PSObject obj = PSObject.AsPSObject(errorRecord, true);
This uses this internal PSObject.ASPSObject() overload, which gives the resulting PSObject instance a wrapper-local ETS type-name list not tied to the original object.
Select-Object -ExpandProperty apparently uses the same overload - obscurely.
Missed it by _that_ much. Nicely done @mklement0 馃榿
Excellent, I'll try this out.
:tada:This issue was addressed in #11125, which has now been successfully released as v7.0.0-rc.2.:tada:
Handy links:
Most helpful comment
You were on the right track, @vexx32, all that was missing was
true:This uses this
internalPSObject.ASPSObject()overload, which gives the resultingPSObjectinstance a wrapper-local ETS type-name list not tied to the original object.Select-Object -ExpandPropertyapparently uses the same overload - obscurely.