Skip to content

Commit 2cc98b1

Browse files
committed
[pwsh profile] Add Expand-Object.
1 parent bb9309b commit 2cc98b1

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

PowerShell/Profile/Common/.files

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
statements.ps1
2+
Expand-Object.ps1
23
Get-Profile.ps1
34
Get-StrictMode.ps1
45
Import-BatchEnvironment.ps1
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Copyright (c) 2023 Matthias Wolf, Mawosoft.
2+
3+
<#
4+
.SYNOPSIS
5+
Normalizes all objects in a collection to have identical sets of properties.
6+
.OUTPUTS
7+
Objects, each with the same set of properties.
8+
.NOTES
9+
Passing a collection through Expand-Object ensures that cmdlets like Export-Csv or Out-GridView
10+
won't miss any properties that only appear on objects further down the pipeline.
11+
By default, Expand-Object will flatten nested collections and convert dictionary-like objects
12+
into custom objects. This can be prevented by using the -NoDictionary and -NoFlatten parameters.
13+
Dictionary conversion: [pscustomobject]@{ foo = 'bar'; baz = 'buzz' }
14+
Note that if you don't also specify -NoFlatten, dictionaries will still be unrolled as key/value pairs:
15+
[pscustomobject]@{ Key = 'foo'; Value = 'bar' }, [pscustomobject]@{ Key = 'baz'; Value = 'buzz' }
16+
#>
17+
function Expand-Object {
18+
[CmdletBinding(PositionalBinding = $false)]
19+
[OutputType([psobject])]
20+
param(
21+
# The objects to process. It is recommended to use the pipeline for passing objects.
22+
[Parameter(ValueFromPipeline)]
23+
[psobject]$InputObject,
24+
# Prevents the conversion of dictionary-like objects (e.g. hashtable) into custom objects.
25+
[switch]$NoDictionary,
26+
# Prevents the flattening of nested collections.
27+
[switch]$NoFlatten
28+
)
29+
begin {
30+
31+
class Processor {
32+
[System.Collections.Generic.List[psobject]]$Items
33+
[System.Collections.Generic.HashSet[string]]$PropertyNames
34+
[bool]$IsExpanded
35+
hidden [bool]$NoDictionary
36+
hidden [bool]$NoFlatten
37+
hidden [System.Collections.Generic.HashSet[string]]$CurrentPropertyNames
38+
39+
Processor([bool] $noDictionary, [bool]$noFlatten) {
40+
$this.Items = [System.Collections.Generic.List[psobject]]::new()
41+
$this.PropertyNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
42+
$this.NoDictionary = $noDictionary
43+
$this.NoFlatten = $noFlatten
44+
$this.CurrentPropertyNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
45+
}
46+
47+
[void]ProcessItems([psobject]$object) {
48+
if ($null -eq $object) { return }
49+
if (-not $this.NoDictionary -and $object -is [System.Collections.IDictionary]) {
50+
$object = [pscustomobject][hashtable]$object
51+
$this.IsExpanded = $true
52+
}
53+
if (-not $this.NoFlatten -and $object -is [System.Collections.ICollection]) {
54+
foreach ($item in ([System.Collections.IEnumerable]$object).GetEnumerator()) {
55+
$this.ProcessItems($item)
56+
}
57+
}
58+
else {
59+
if ($this.IsExpanded -or $this.PropertyNames.Count -eq 0) {
60+
foreach ($property in $object.psobject.Properties) {
61+
$this.PropertyNames.Add($property.Name)
62+
}
63+
}
64+
else {
65+
$this.CurrentPropertyNames.Clear()
66+
foreach ($property in $object.psobject.Properties) {
67+
$this.CurrentPropertyNames.Add($property.Name)
68+
}
69+
if (-not $this.PropertyNames.SetEquals($this.CurrentPropertyNames)) {
70+
$this.IsExpanded = $true
71+
$this.PropertyNames.UnionWith($this.CurrentPropertyNames)
72+
}
73+
}
74+
$this.Items.Add($object)
75+
}
76+
}
77+
[System.Collections.ICollection]GetItems() {
78+
if (-not $this.IsExpanded) {
79+
return $this.Items
80+
}
81+
[System.Collections.Generic.List[psobject]]$newItems = [System.Collections.Generic.List[psobject]]::new($this.Items.Count)
82+
foreach ($item in $this.Items) {
83+
# PowerShell 5.1 compat: Not available: [psobject]::new($this.PropertyNames.Count)
84+
[psobject]$newItem = [psobject]::new()
85+
$this.CurrentPropertyNames.Clear()
86+
foreach ($property in $item.psobject.Properties) {
87+
$this.CurrentPropertyNames.Add($property.Name)
88+
$newItem.psobject.Properties.Add([psnoteproperty]::new($property.Name, $property.Value))
89+
}
90+
foreach ($property in $this.PropertyNames) {
91+
if (-not $this.CurrentPropertyNames.Contains($property)) {
92+
$newItem.psobject.Properties.Add([psnoteproperty]::new($property, $null))
93+
}
94+
}
95+
$newItems.Add($newItem)
96+
}
97+
return $newItems
98+
}
99+
}
100+
101+
[Processor]$processor = [Processor]::new($NoDictionary, $NoFlatten)
102+
}
103+
process {
104+
$processor.ProcessItems($InputObject)
105+
}
106+
end {
107+
$PSCmdlet.WriteObject($processor.GetItems(), $true)
108+
}
109+
}

0 commit comments

Comments
 (0)