Public/Format-PSObject.ps1

<#
    .SYNOPSIS
        Displays an object's nested properties and their values

    .DESCRIPTION
        Expands any complex PowerShell object for analysis or comparison. Format-PSObject returns
        a list of dot walked paths for each of the object's properties, along with values, up to
        the depth limit.

    .PARAMETER InputObject
        The object to format

    .PARAMETER Depth
        The recursive limit for nested object property values

    .PARAMETER IgnoreProperty
        An array of property names to skip formatting

    .PARAMETER MergeArrays
        Uses array values directly, instead of expanding the contents and index of the individual array items

    .PARAMETER Parent
        For internal use, but you can specify the base name of the variable displayed in the path

    .PARAMETER CurrentDepth
        For internal use

    .EXAMPLE
        Format-PSObject -InputObject @{value1 = 'testvalue'; value2 = @{nestedValue = 'nested'}}

        Create a property mapping of the 'InputObject' properties and their respective values

    .NOTES
        Adapted from https://www.red-gate.com/simple-talk/blogs/display-object-a-powershell-utility-cmdlet/
#>

function Format-PSObject {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$InputObject,

        [Parameter()]
        [int]$Depth = 5,

        [Parameter()]
        [string[]]$IgnoreProperty,

        [Parameter()]
        [switch]$MergeArrays,

        [Parameter(DontShow)]
        [string]$Parent = '$_',

        [Parameter(DontShow)]
        [int]$CurrentDepth = 0
    )

    begin {
        if ($CurrentDepth -eq 0) { $reachedDepthLimit = $false }
    }

    process {
        if ($CurrentDepth -ge $Depth) {
            # Prevent runaway recursion
            $Script:reachedDepthLimit = $true
        }

        if ($null -eq $InputObject) {
            return $null
        }

        $ObjectTypeName = $InputObject.GetType().Name

        Write-Verbose "[$Parent] ObjectType = $ObjectTypeName"

        if ($ObjectTypeName -in 'HashTable', 'OrderedDictionary') {
            # If you can, force it to be a PSCustomObject in order to enable iterating over the properties
            $InputObject = [pscustomObject]$InputObject
            $ObjectTypeName = 'PSCustomObject'
        }

        if (-not ($InputObject.Count -gt 1) -and (-not ($InputObject -is [System.Collections.IEnumerable]))) {
            if ($ObjectTypeName -in @('PSCustomObject')) {
                $MemberType = 'NoteProperty'
            } else {
                $MemberType = 'Property'
            }

            if ($InputObject) {
                Get-Member -InputObject $InputObject -MemberType $MemberType -Force |
                Where-Object { $_.Name -notin $IgnoreProperty } |
                ForEach-Object {
                    $property = $_

                    try {
                        $child = $InputObject.($property.Name)
                        $psTypeName = try { $Child.GetType() } catch { $null }
                    } catch {
                        # Prevent crashing on write-only objects
                        $child = $null
                    }

                    if (
                        $child -eq $null -or
                        $child.GetType().BaseType.Name -eq 'ValueType' -or
                        $child.GetType().Name -in @('String', 'String[]')
                    ) {
                        [pscustomobject]@{
                            'Path'       = "$Parent.$($property.Name)"
                            'Value'      = $Child
                            'PSTypeName' = $psTypeName
                            'Depth'      = $CurrentDepth
                        }
                    } elseif (($CurrentDepth + 1) -eq $Depth) {
                        $Script:reachedDepthLimit = $true
                        [pscustomobject]@{
                            'Path'       = "$Parent.$($property.Name)"
                            'Value'      = $Child
                            'PSTypeName' = $psTypeName
                            'Depth'      = $CurrentDepth
                        }
                    } else {
                        $FormatPSObjectParams = @{
                            InputObject    = $child
                            depth          = $Depth
                            IgnoreProperty = $IgnoreProperty
                            Parent         = "$Parent.$($property.Name)"
                            CurrentDepth   = $currentDepth + 1
                            MergeArrays    = $MergeArrays
                        }
                        Format-PSObject @FormatPSObjectParams
                    }
                }
            }
        } else {
            if ($MergeArrays) {
                $psTypeName = try { $Child.GetType() } catch { $null }
                [pscustomobject]@{
                    'Path'       = "$Parent"
                    'Value'      = $InputObject | Sort-Object
                    'PSTypeName' = $psTypeName
                    'Depth'      = $CurrentDepth
                }
            } else {
                0..($InputObject.Count - 1) | ForEach-Object {
                    $iterator = $_
                    $child = $InputObject[$iterator]
                    $psTypeName = try { $Child.GetType() } catch { $null }

                    if (
                        ($null -eq $child) -or #is the current child a value or a null?
                        ($child.GetType().BaseType.Name -eq 'ValueType') -or
                        ($child.GetType().Name -in @('String', 'String[]'))
                    ) {
                        [pscustomobject]@{
                            'Path'       = "$Parent[$iterator]"
                            'Value'      = "$child"
                            'PSTypeName' = $psTypeName
                            'Depth'      = $CurrentDepth
                        }
                    } elseif (($CurrentDepth + 1) -eq $Depth) {
                        $Script:reachedDepthLimit = $true
                        [pscustomobject]@{
                            'Path'       = "$Parent[$iterator]"
                            'Value'      = "$child"
                            'PSTypeName' = $psTypeName
                            'Depth'      = $CurrentDepth
                        }
                    } else {
                        $FormatPSObjectParams = @{
                            InputObject    = $child
                            depth          = $Depth
                            IgnoreProperty = $IgnoreProperty
                            Parent         = "$Parent[$iterator]"
                            CurrentDepth   = $currentDepth + 1
                        }
                        Format-PSObject @FormatPSObjectParams
                    }
                }
            }
        }
    }

    end {
        if ($CurrentDepth -eq 0 -and $reachedDepthLimit) {
            Write-Warning (
                "Format-PSObject reached the depth limit [$Depth]. " +
                'Use the -Depth parameter to increase the recursion limit.'
            )
        }
    }
}