Evotec Services sp. z o.o., ul. Drozdów 6, Mikołów, 43-190, Poland

PowerShell – Converting advanced object to flat object

ConvertTo-FlatObject

PowerShell language allows you to work and build complicated objects. There are multiple ways to save them, such as XML or JSON, but sometimes using them is impossible or inadequate. Sometimes you want to use HTML or CSV or any other single dimension output.

HTML Export for nested objects

Let's say you have an advanced PowerShell object with nested properties. It's a great feature of PowerShell, but displaying it to the user saving it as CSV or HTML will give you results that will not be very useful.

$Object3 = [PSCustomObject] @{
    "Name"    = "Przemyslaw Klys"
    "Age"     = "30"
    "Address" = @{
        "Street"  = "Kwiatowa"
        "City"    = "Warszawa"

        "Country" = [ordered] @{
            "Name" = "Poland"
        }
        List      = @(
            [PSCustomObject] @{
                "Name" = "Adam Klys"
                "Age"  = "32"
            }
            [PSCustomObject] @{
                "Name" = "Justyna Klys"
                "Age"  = "33"
            }
        )
    }
    ListTest  = @(
        [PSCustomObject] @{
            "Name" = "Justyna Klys"
            "Age"  = "33"
        }
    )
}

$Object4 = [PSCustomObject] @{
    "Name"    = "Przemyslaw Klys"
    "Age"     = "30"
    "Address" = @{
        "Street"  = "Kwiatowa"
        "City"    = "Warszawa"
        "Country" = [ordered] @{
            "Name" = "Gruzja"
        }
        List      = @(
            [PSCustomObject] @{
                "Name" = "Adam Klys"
                "Age"  = "32"
            }
            [PSCustomObject] @{
                "Name" = "Pankracy Klys"
                "Age"  = "33"
            }
            [PSCustomObject] @{
                "Name" = "Justyna Klys"
                "Age"  = 30
            }
            [PSCustomObject] @{
                "Name" = "Justyna Klys"
                "Age"  = $null
            }
        )
    }
    ListTest  = @(
        [PSCustomObject] @{
            "Name" = "Sława Klys"
            "Age"  = "33"
        }
    )
    MoreProperties = $true
}
$Object3, $Object4 | Out-HtmlView -Filtering

While Name and Age properties output was proper, the rest – not so much.

Export-CSV for nested objects

The same problem will be visible in the Export-CSV command.

$Object3, $Object4 | Export-Csv -Path "$PSScriptRoot\test.csv" -NoTypeInformation
Converting Advanced PowerShell object to flat objects

While you can always use JSON and XML as mentioned to save files – wouldn't it be nice to have another option? This is where ConvertTo-FlatObject comes in! It converts advanced objects into flat ones.

$Object3, $Object4 | ConvertTo-FlatObject | Export-Csv -Path "$PSScriptRoot\test.csv" -NoTypeInformation -Encoding UTF8

The same goes for Out-HTMLView and New-HTMLTable from PSWriteHTML module. As part of the module, I've added a parameter called FlattenObject which internally uses ConvertTo-FlatObject to make it easy for end-users.

$Object3, $Object4 | Out-HtmlView -Filtering -FlattenObject
Office 365 Example of using ConvertTo-FlatObject inside Out-HTMLView

If you're wondering what it could be useful for – let me give you an example. Get-AzureADAuditSignInLogs is a cmdlet from Office 365 to get information about logging in users to Office 365.

Connect-AzureAD
$Logins = Get-AzureADAuditSignInLogs
$Logins[0]

This means that once you push it to CSV or HTML using Out-HTMLView, all those nested properties would be hidden away or mainly unreadable, unsortable.

As you see above – it's far from perfect. I would usually take my PowerShell skills and convert those advanced objects into smaller, one-dimensional objects to display exactly what I wanted. If I had 50 columns, I would rebuild the thing entirely, and it would take some time to do it. Or I would use new functionality and just like that, with a single line of code get it all in one go.

$Logins | Out-HtmlView -ScrollX -Filtering -FlattenObject
$Logins | Out-HtmlView -ScrollX -Filtering -FlattenObject -ExcludeProperty "*IsReadOnly","*Count"

Notice that I'm using ExcludeProperty to filter out some columns that are often part of the object that I don't want to see. Isn't it much nicer? Easily readable, zero effort. Report ready in 15 minutes.

Installing PSSharedGoods

ConvertTo-FlatObject is part of the PSSharedGoods PowerShell module. This is my “glue” module with loads of functions. It contains over 200 functions that allow me to reuse those when needed. How do you install it? The easiest and most optimal way is to use PowerShellGallery. This will get you up and running in no time. Whenever there is an update, just run Update-Module, and you're done.

Install-Module PSSharedGoods
# Update-Module PSSharedGoods

If you prefer New-HTMLTable or Out-HTMLView commands, you may want to install PSWriteHTML instead.

Install-Module PSWriteHTML
# Update-Module PSWriteHTML

However, if you're into code – want to see how everything is done, you can use GitHub sources. Please keep in mind that the PowerShellGallery version is optimized and better for production use. If you see any issues, bugs, or features that are missing, please make sure to submit them on GitHub.

Direct source code for ConvertTo-FlatObject

If you don't like using modules, you can use code directly.

Function ConvertTo-FlatObject {
    <#
    .SYNOPSIS
    Flattends a nested object into a single level object.

    .DESCRIPTION
    Flattends a nested object into a single level object.

    .PARAMETER Objects
    The object (or objects) to be flatten.

    .PARAMETER Separator
    The separator used between the recursive property names

    .PARAMETER Base
    The first index name of an embedded array:
    - 1, arrays will be 1 based: <Parent>.1, <Parent>.2, <Parent>.3, …
    - 0, arrays will be 0 based: <Parent>.0, <Parent>.1, <Parent>.2, …
    - "", the first item in an array will be unnamed and than followed with 1: <Parent>, <Parent>.1, <Parent>.2, …

    .PARAMETER Depth
    The maximal depth of flattening a recursive property. Any negative value will result in an unlimited depth and could cause a infinitive loop.

    .PARAMETER Uncut
    The maximal depth of flattening a recursive property. Any negative value will result in an unlimited depth and could cause a infinitive loop.

    .EXAMPLE
    $Object3 = [PSCustomObject] @{
        "Name"    = "Przemyslaw Klys"
        "Age"     = "30"
        "Address" = @{
            "Street"  = "Kwiatowa"
            "City"    = "Warszawa"

            "Country" = [ordered] @{
                "Name" = "Poland"
            }
            List      = @(
                [PSCustomObject] @{
                    "Name" = "Adam Klys"
                    "Age"  = "32"
                }
                [PSCustomObject] @{
                    "Name" = "Justyna Klys"
                    "Age"  = "33"
                }
                [PSCustomObject] @{
                    "Name" = "Justyna Klys"
                    "Age"  = 30
                }
                [PSCustomObject] @{
                    "Name" = "Justyna Klys"
                    "Age"  = $null
                }
            )
        }
        ListTest  = @(
            [PSCustomObject] @{
                "Name" = "Sława Klys"
                "Age"  = "33"
            }
        )
    }

    $Object3 | ConvertTo-FlatObject

    .NOTES
    Based on https://powersnippets.com/convertto-flatobject/
    #>
    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeLine)][Object[]]$Objects,
        [String]$Separator = ".",
        [ValidateSet("", 0, 1)]$Base = 1,
        [int]$Depth = 5,
        [Parameter(DontShow)][String[]]$Path,
        [Parameter(DontShow)][System.Collections.IDictionary] $OutputObject
    )
    Begin {
        $InputObjects = [System.Collections.Generic.List[Object]]::new()
    }
    Process {
        foreach ($O in $Objects) {
            $InputObjects.Add($O)
        }
    }
    End {
        If ($PSBoundParameters.ContainsKey("OutputObject")) {
            $Object = $InputObjects[0]
            $Iterate = [ordered] @{}
            if ($null -eq $Object) {
                #Write-Verbose -Message "ConvertTo-FlatObject - Object is null"
            } elseif ($Object.GetType().Name -in 'String', 'DateTime', 'TimeSpan', 'Version', 'Enum') {
                $Object = $Object.ToString()
            } elseif ($Depth) {
                $Depth--
                If ($Object -is [System.Collections.IDictionary]) {
                    $Iterate = $Object
                } elseif ($Object -is [Array] -or $Object -is [System.Collections.IEnumerable]) {
                    $i = $Base
                    foreach ($Item in $Object.GetEnumerator()) {
                        $Iterate["$i"] = $Item
                        $i += 1
                    }
                } else {
                    foreach ($Prop in $Object.PSObject.Properties) {
                        if ($Prop.IsGettable) {
                            $Iterate["$($Prop.Name)"] = $Object.$($Prop.Name)
                        }
                    }
                }
            }
            If ($Iterate.Keys.Count) {
                foreach ($Key in $Iterate.Keys) {
                    ConvertTo-FlatObject -Objects @(, $Iterate["$Key"]) -Separator $Separator -Base $Base -Depth $Depth -Path ($Path + $Key) -OutputObject $OutputObject
                }
            } else {
                $Property = $Path -Join $Separator
                $OutputObject[$Property] = $Object
            }
        } elseif ($InputObjects.Count -gt 0) {
            foreach ($ItemObject in $InputObjects) {
                $OutputObject = [ordered]@{}
                ConvertTo-FlatObject -Objects @(, $ItemObject) -Separator $Separator -Base $Base -Depth $Depth -Path $Path -OutputObject $OutputObject
                [PSCustomObject] $OutputObject
            }
        }
    }
}

Posty powiązane