Last active
September 27, 2024 13:47
-
-
Save jhoneill/7ac346d23f850bd2d09a99bd66fde39e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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