Skip to content

Instantly share code, notes, and snippets.

@jhoneill
Last active September 27, 2024 13:47
Show Gist options
  • Save jhoneill/7ac346d23f850bd2d09a99bd66fde39e to your computer and use it in GitHub Desktop.
Save jhoneill/7ac346d23f850bd2d09a99bd66fde39e to your computer and use it in GitHub Desktop.
function Expand-PropertyTree {
<#
.Synopis
Unrolls an object with many sub-objects.
.Example
C:> Get-Process -id $PID | Expand-PropertyTree | ft -a Path,value
Outputs the paths and values for the unrolled properties for a process object and displays path and value as a table.
By default the modules and threads properties are limited to showing on 2 of their many members
We can use -MaxArrayItems to get a different number 0 won't expand at all, and a negative number will expand ALL of them.
.Example
C:> Get-Process -id $PID | Expand-PropertyTree -NotMatch 'module|parent|threads' | ft -a Path,value
Builds on the previous example but filters out any objects containing module,parent or threads.
ModuleWill filterout "MainModule" we could use "^module" in the regular expression to avoid this.
.Example
C:> $p = Get-Process -id $PID | Expand-PropertyTree -NotMatch 'module|parent|threads'
C:> Select-Tree -Items $p -StartAt $p[0] -multi
Renders the expanded properties as a tree to select items.
.Example
C:> $hash = [ordered]@{}
C:> Get-Process -id $PID | Expand-PropertyTree -maxArrrayItems 0 -Separator '_' | where {$null -ne $_.value} | foreach {$hash[$_.path.Trim("_")] = $_.value}
Switches the separator to "_" and builds a hash of populated property paths to values - trimming the leading _ from the path
This hash could be converted into a [pscustomObject]
#>
param (
[Parameter(position=0,ValueFromPipeline=$true,Mandatory=$true )]
$InputObject,
[Parameter(Position=1 )]
$Separator ='.' ,
[Parameter(DontShow=$true)]
$MaxDepth = 5,
$MaxArrayItems = 2,
$NotMatch,
[switch]$NoIndent,
[Parameter(DontShow=$true)]
$Prefix
)
process {
#Default is in
#Steps we can go down the hierarchy - when unrolling arrays this can become negative so make sure it doesn't go below zero
$MaxDepth --
if ($MaxDepth -lt 0) {$MaxDepth = 0}
$indent = ($MaxDepth +1) * 2 * (-not $NoIndent)
$parent = $Prefix
#Create an object for the root.
if ($Prefix -eq $Separator -or -not $Prefix ) {
[pscustomObject]([Ordered]@{Path=$Separator; Label=$Separator; Parent = $null ; Value = $null})
$Prefix = $Separator
}
#for simplicity if we got a hash table, make it an object
If ($InputObject -is [System.Collections.Specialized.OrderedDictionary] -or $InputObject -is [hashtable]) {
$InputObject = [pscustomobject]$InputObject
}
#Traverse the property tree.
#For string, value types (numbers, dates and boolean) and null output them
#For array properties output "Name" then for each one out put "Index", then that object call ourselves with the object at that index
#For all others ouput name and call ourselves with that object.
$InputObject.psobject.properties.Foreach({
$Path = $parent + $Separator + $_.Name
if ($NotMatch -and $_.Name -match $NotMatch ) {
#return nothing
}
elseif ($_.value -is [System.ValueType] -or $_.value -is [System.String] -or $null -eq $_.value) {
[pscustomobject]([Ordered]@{Path=$path; Label="{0,-40}$(' ' * $indent) {1}" -f $_.Name, ($_.value -replace '^(.{120}).*','$1...'); parent = $Prefix; value = $_.value })
}
elseif ($_.value -is [System.Collections.IEnumerable]) {
[pscustomobject]([ordered]@{Path=$Path; Label=$_.name; Parent = $Prefix; Value=$null})
#We want the index of items and to bail when we hit too many so use a while instead of a foreach
$Count = 0
while (($MaxArrayItems -lt 0 -or $count -lt $MaxArrayItems) -and $Count -lt $_.value.count) {
[pscustomobject]([ordered]@{path=($path + "[" + $count+ "]"); Label = $count; Parent=$path; Value = $Null})
if ($MaxDepth -gt 0) {
Expand-PropertyTree -InputObject $_.value[$count] -Separator $Separator -MaxDepth ($MaxDepth -1 ) -MaxArrayItems $MaxArrayItems -NoIndent:$NoIndent -Prefix ($path + "[" + $count+ "]")
}
$count ++
}
}
elseif ($MaxDepth -gt 0) { #it may be negative!
[pscustomobject]([ordered]@{path=$path; Label = $_.Name; Parent=$Prefix; Value = $Null})
Expand-PropertyTree -InputObject $_.value -Separator $Separator -MaxDepth $MaxDepth -MaxArrayItems $MaxArrayItems -NoIndent:$NoIndent -Prefix $path
}
#xxx Todo ? A final else warn the first time depth exceeds max depth? But this will mess up the tree use ?
})
}
}
function Select-Tree {
param (
[Parameter(Mandatory=$true)]$Items,
[Parameter(Mandatory=$true)]$StartAt,
[string]$Path="Path",
[string]$Parent="Parent",
[string]$Label="Label",
$Indent=0,
[switch]$Multiple,
[switch]$Alphabetical
)
if ($Indent -eq 0) {$script:treeCounter = -1 ; $script:treeList=@()}
$script:treeCounter++
$script:treeList = $script:treeList + @($StartAt)
$children = $Items | Where-Object {$_.$Parent -eq $StartAt.$Path.ToString()}
if ($null -eq $children) {
"{0,-4} {1}├──{2} " -f $script:treeCounter, ("" * ($Indent-1)) , $StartAt.$Label | Out-Host
}
else {
"{0,-4} {1}┼{2} " -f $script:treeCounter, ("" * ($Indent)) , $StartAt.$Label | Out-Host
if ($Alphabetical) {$children = $children | Sort-Object $Label }
$children | ForEach-Object {
Select-Tree -Items $Items -StartAt $_ -Path $Path -parent $Parent -label $Label -indent ($Indent+1)
}
}
if ($Indent -eq 0) {
if ($multiple) { $response = Read-Host -Prompt "Which one(s) ?" }
else { $response = Read-Host -Prompt "Which one ?" }
if ($response -gt "") {
if ($multiple) { $response.Split(",") | ForEach-Object -Begin {$r = @()} -process {
if ($_ -match "^\d+$") {$r += $_}
elseif ($_ -match "^\d+\.\.\d+$") {$r += (Invoke-Command -ScriptBlock ([scriptblock]::Create( $_)))}} -end {$script:treeList[$r] }}
else { $script:treeList[$response] }
}
}
}
function Get-PropList {
<#
.Synopis
Creates code snippets to work with awkward hierarchical objects
.Example
C:> $APIResponse = Invoke-RestMethod «API Call Parameters»
C:> $APIResponse[1] | Get-PropList -MaxArrayItems 1 -SelectFormat
Will produce a section like list
0 +.
1 | +DataProvider
2 | |--WebsiteURL http://openchargemap.org
3 | |--Comments
14 | |--Title Open Charge Map Contributors
15 | +OperatorInfo
16 | |--WebsiteURL xxxx
17 | |--Comments xxx
26 | |--ID 3
27 | |--Title xxx
55 |--UsageTypeID 4
56 |--UsageCost £0.30/kWh
57 | +AddressInfo
60 | |--AddressLine1 xxx
66 | | +Country
70 | | |--Title United Kingdom
71 | |--Latitude 51.623854
72 | |--Longitude -1.296356
80 | +Connections
81 | | +0
82 | | |--ID 176884
83 | | |--ConnectionTypeID 25
84 | | | +ConnectionType
89 | | | |--Title Type 2 (Socket Only)
105 | | |--PowerKW 22
113 |--NumberOfPoints 2
Which one(s) ?:
Selecting 27,16,60..64,70,89,105,14 will return
@{n='OperatorInfo_Title' ;e={ $_.OperatorInfo.Title }},
@{n='OperatorInfo_WebsiteURL' ;e={ $_.OperatorInfo.WebsiteURL }},
@{n='AddressInfo_AddressLine1' ;e={ $_.AddressInfo.AddressLine1 }},
@{n='AddressInfo_AddressLine2' ;e={ $_.AddressInfo.AddressLine2 }},
@{n='AddressInfo_Town' ;e={ $_.AddressInfo.Town }},
@{n='AddressInfo_StateOrProvince' ;e={ $_.AddressInfo.StateOrProvince }},
@{n='AddressInfo_Postcode' ;e={ $_.AddressInfo.Postcode }},
@{n='AddressInfo_Country_Title' ;e={ $_.AddressInfo.Country.Title }},
@{n='Connections_0__ConnectionType_Title' ;e={ $_.Connections[0].ConnectionType.Title }},
@{n='Connections_0__PowerKW' ;e={ $_.Connections[0].PowerKW }},
@{n='DataProvider_Title' ;e={ $_.DataProvider.Title }},
Which might look like this in the final code.
$apiresponse | select-object @(
@{n='OperatorName' ;e={ $_.OperatorInfo.Title }},
@{n='OperatorWebsite' ;e={ $_.OperatorInfo.WebsiteURL }},
@{n='AddressLine1' ;e={ $_.AddressInfo.AddressLine1 }},
@{n='AddressLine2' ;e={ $_.AddressInfo.AddressLine2 }},
@{n='Town' ;e={ $_.AddressInfo.Town }},
@{n='StateOrProvince' ;e={ $_.AddressInfo.StateOrProvince }},
@{n='Postcode' ;e={ $_.AddressInfo.Postcode }},
@{n='Country' ;e={ $_.AddressInfo.Country.Title }},
@{n='ConnectionType' ;e={($_.Connections.ConnectionType.Title | Sort-Object -Unique) -Join "," }},
@{n='MaxPowerKW' ;e={($_.Connections.PowerKW | Measure-object -Max).Maximum }},
@{n='DataProvider' ;e={ $_.DataProvider.Title }}
)
#>
param(
[Parameter(position=0,ValueFromPipeline=$true,Mandatory=$true)]
$InputObJect,
$Separator ='.' ,
$MaxDepth = 5,
$MaxArrayItems = 2,
$NotMatch,
[switch]$SelectFormat,
[switch]$HashFormat
)
$params = @{} + $PSBoundParameters
$null = $params.Remove('SelectFormat'),$params.Remove('HashFormat')
$properties = Expand-PropertyTree @params
if ($SelectFormat) {
"Select-Object -Property @(`r`n" +
((Select-Tree -Items $properties -StartAt $properties[0] -Multiple | ForEach-Object {' @{{n={1,-60};e={{ $_{0,-60} }}}}' -f $_.path, "'$($_.path -replace '^\W','' -replace '\W',"_" )'" }) -join ",`r`n" ) +
"`r`n)"
}
elseif ($HashFormat) {
"`$h = @{`r`n" +
((Select-Tree -Items $properties -StartAt $properties[0] -Multiple | ForEach-Object { ' {1,-60} $_{0,-60}' -f $_.path, "'$($_.path -replace '^\W','' -replace '\W',"_" )'=" }) -join ";`r`n" ) +
"`r`n}@"
}
else {((Select-Tree -Items $properties -StartAt $properties[0] -Multiple | ForEach-Object { '$_{0,-60},' -f $_.path }) -join ",`r`n" ) }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment