Search code examples
powershellhashtablereverseenumerate

How to reverse traverse a nested hashtable in PowerShell


In a previous question, I was given a solution for finding the name of a key by its value. Unfortunately I neglected to consider the application of Security Groups

Old hashtable and working function

$Departments = @{
    'Sales' = @{
        'SAM' = 'Manager'
        'SAP' = 'Person'
    }
    'IT'    = @{
        'ITM' = 'Manager'
        'ITS' = 'Specialist'
        'ITT' = 'Technician'
        'ITC' = 'Consultant'     
    }
}

function Get-DepartmentOU {
    Param (
        [CmdletBinding()]
        [Parameter(Mandatory = $true)]
        [System.String]
        $RoleCode
    )

    # Get the DictionaryEntry in the main Hashtable where the nested Hashtable value matches the role you are looking for.
    $Department = $script:Departments.GetEnumerator() | Where-Object { $_.Value.ContainsKey($RoleCode) }

    # Print the name of the DictionaryEntry (Your department) and retrieve the value from the Hashtable for the role.
    "Department: $($Department.Name) Job Title: $($Department.Value[$RoleCode])" 

}


$JobCode = "SAM"
$DepartmentInfo = Get-DepartmentOU -RoleCode $JobCode
$DepartmentInfo

OUTPUT: Department: Sales Job Title: Manager

The above works excellently, however I've now created a deeper hashtable and need to do the same thing, just another level to extract more information.

New hashtable

$Departments = @{
    "Parts" = @{
        "SG-Parts" = @{
            "PAM" = "Parts Manager"
            "PAA" = "Parts Advisor"
        }
    }
    "Sales" = @{
        "SG-Sales"          = @{
            "SAP" = "Sales Person"
            "SAR" = "Receptionist"
        }
        "SG-Sales Managers" = @{
            "SGM" = "General Manager"
            "SAM" = "Sales Manager"
        }

    }
}

How should I change the working function to display the text contained in the key

OUTPUT: SG: SG-Sales Managers Department: Sales Job Title: Manager


This may help to visualize the data structure that @mathias-r.-jessen's 'flat role table/map' code generates:

$Rolemap = @{
    "PAC" = @{
        "Title"      = "Parts Consultant"
        "SG"         = "SG-Parts"
        "Department" = "Parts"
    }
    "PAA" = @{
        "Title"      = "Parts Advisor"
        "SG"         = "SG-Parts"
        "Department" = "Parts"
    }
    "SGM" = @{
        "Title"      = "General Manager"
        "SG"         = "SG-Sales Managers"
        "Department" = "Sales"
    }
}

The original data structure is quicker for humans to modify as needed as it mimics a common small-scale Active Directory setup.

When using the original data format, each new role addition only has to be added to the correct 'group' within the hashtable. However, reversing the "SAM" = "Sales Manager" to "Sales Manager"="SAM" may help for legibility and logic / structure.

I used $RoleMap |Format-Custom and some manual typing to build the generated table visualization text.


Solution

  • Create a new flat role table from your OU-like hashtable structure:

    $RoleMap = @{}
    
    foreach($departmentCode in $Departments.psbase.Keys){
        foreach($sgCode in $Departments[$departmentCode].psbase.Keys){
            foreach($roleCode in $Departments[$departmentCode][$sgCode].psbase.Keys){
                # Create a summary object that includes all 3 pieces of information
                # Store in role table and use the "role code" as the key
                $RoleMap[$roleCode] = [pscustomobject]@{
                    Title = $Departments[$departmentCode][$sgCode][$roleCode]
                    SG = $sgCode
                    Department = $departmentCode
                }
            }
        }
    }
    

    Now you can avoid ...GetEnumerator() | Where-Object { ... } completely when resolving the role code:

    function Get-DepartmentOU {
        param(
            [CmdletBinding()]
            [Parameter(Mandatory = $true)]
            [string]
            $RoleCode
        )
    
        if($script:RoleMap.Contains($RoleCode)){
            # `Where-Object` no longer needed!
            $roleDescription = $script:RoleMap[$RoleCode]
    
            "SG: $($roleDescription.SG) Department: $($roleDescription.Name) Job Title: $($roleDescription.Title)"
        }
    }