Dynamically build group names with a hashtable and string array

This topic contains 7 replies, has 3 voices, and was last updated by Profile photo of Jamie Nelson Jamie Nelson 1 year, 8 months ago.

  • Author
    Posts
  • #31958
    Profile photo of Jamie Nelson
    Jamie Nelson
    Participant

    Hi all, been banging my head against the wall trying to figure out the easiest way to dynamically generate a list of groups using unique attribute values I pull from my Active Directory user objects and store in a hashtable. Then I have a string array (supplied as a parameter) populated with "templates" corresponding to how I want the group names to look. Each template may have one or more "tokens" in it which correspond to Active Directory attribute values stored in the hashtable. I am trying to find the best way to do all of this dynamically so that I don't have to hard code in any logic, text delimiters, etc. Preferably I would like to use a regex to iterate through the "templates" and find each attribute match, then lookup that attribute in the hastable and build out all of the unique group names that would result. I've tried doing this several different ways using nested loops, etc, but can't quite get it. In the interest of simplicity I'm including sample data below with the specific output I'm looking for. Any help would be greatly appreciated!

    
    # Sample attributes; I'm actually pulling these directly from Active Directory and dynamically creating a hashtable
    $attributes=@{
        division=@("US","Europe")
        department=@("Information Technology","Human Resources","Accounting","Finance")
        employeeType=@("Employee","Contractor")
    }
    
    # Sample group name templates; These would be manually supplied via a Script Parameter
    $templates=@(
        "role.[division].[department]",
        "role.contoso.[department]",
        "role.contoso.[employeeType]"
    )
    
    $regex_attributename = "\[([a-zA-Z0-9]+)\]"
    $groupnames = @()
    
    #
    

    ex. values of $groupnames I'm trying to build dynamically:
    role.us.informationtechnology
    role.us.humanresources
    role.us.accounting
    role.us.finance
    role.europe.informationtechnology
    role.europe.humanresources
    role.europe.accounting
    role.europe.finance
    role.contonso.informationtechnology
    role.contoso.humanresources
    role.contoso.accounting
    role.contoso.finance
    role.contoso.employee
    role.contoso.contractor

  • #31960
    Profile photo of Curtis Smith
    Curtis Smith
    Participant

    Like this

    # Sample attributes; I'm actually pulling these directly from Active Directory and dynamically creating a hashtable
    $attributes=@{
        division=@("US","Europe")
        department=@("Information Technology","Human Resources","Accounting","Finance")
        employeeType=@("Employee","Contractor")
    }
    
    # "role.[division].[department]",
    ForEach ($division in $attributes.division) {
        ForEach ($department in $attributes.department) {
            "role.$division.$department" -replace " "
        }
    }
    
    # "role.contoso.[department]"
    ForEach ($department in $attributes.department) {
        "role.contoso.$department" -replace " "
    }
    
    # "role.contoso.[employeeType]"
    ForEach ($employeetype in $attributes.employeetype) {
        "role.contoso.$employeetype" -replace " "
    }
    

    Results:
    role.US.InformationTechnology
    role.US.HumanResources
    role.US.Accounting
    role.US.Finance
    role.Europe.InformationTechnology
    role.Europe.HumanResources
    role.Europe.Accounting
    role.Europe.Finance
    role.contoso.InformationTechnology
    role.contoso.HumanResources
    role.contoso.Accounting
    role.contoso.Finance
    role.contoso.Employee
    role.contoso.Contractor

    You can add .tolower() if you you need it all in lower case.

  • #31963
    Profile photo of Jamie Nelson
    Jamie Nelson
    Participant

    Thanks Curtis, I appreciate your response. However, I was actually trying to accomplish this is a much more dynamic fashion, as the templates would be passed as a parameter to the script, so I was trying to avoid hard coding any logic because the templates aren't always going to follow the same format or even use the same AD attributes each time. Does that make sense?

    What I initially attempted was to do was this:

    1. Iterate through $templates and look for matches that follow a token attribute I need to find/replace with the attribute values in $attributes[$attributename]
    2. Using the first template as an example, the first match would result in names that look like "role.us.[department]" or "role.europe.[department]"
    3. Keep processing the name collection until none of the names have any regex matches. This would need to work for templates that could potentially have several different attributes in them.
    4. Process the other name templates in the same fashion

    Here's the code I attempted to use:

    
    # Sample attributes; I'm actually pulling these directly from Active Directory and dynamically creating a hashtable
    $attributes=@{
        division=@("US","Europe")
        department=@("Information Technology","Human Resources","Accounting","Finance")
        employeeType=@("Employee","Contractor")
    }
    
    # Sample group name templates; These would be manually supplied via a Script Parameter
    $templates=@(
        "role.[division].[department]",
        "role.contoso.[department]",
        "role.contoso.[employeeType]"
    )
    
    $regex_attributename = "\[([a-zA-Z0-9]+)\]"
    
    $groupnames = $templates
    do {
        for ($i=0; $i -lt $groupnames.Count; $i++) {
            if ($groupnames[$i] -match $regex_attributename) {
                $groupname = $groupnames[$i]
                $attributename = $Matches[1]
                $attributevals = $attributes[$attributename]
                if ($attributevals.Count -gt 1) {
                    for ($j=0; $j -lt $attributevals.Count; $j++) {
                        switch ($j) {
                            0 {$groupnames[$i] = $groupname -replace "\[$attributename\]", $attributevals[$j]}
                            default {$groupnames += $groupname -replace "\[$attributename\]", $attributevals[$j]}
                        }
                    }
                } else {$groupnames[$i] = $groupname -replace "\[$attributename\]", $attributevals}
            }
        }
    } until ($groupnames -notmatch $regex_attributename) 
    

    This results in $groupnames containing the following strings. I just can't seem to get it to process everything the way I want. Have tried a few different ways but the array always contains some strings that still have token values in them.

    role.US.[department]
    role.contoso.Information Technology
    role.contoso.Employee
    role.Europe.Information Technology
    role.contoso.Human Resources
    role.contoso.Accounting
    role.contoso.Finance
    role.contoso.Contractor
    role.Europe.Human Resources
    role.Europe.Accounting
    role.Europe.Finance

  • #31970
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    What about something like this?

    function MakeGroupNames {
        [CmdletBinding()]
        param(
            [Parameter(Mandatory, ValueFromPipeline)]
            $TemplateString,
            [Parameter(Mandatory)]
            [hashtable] $Attributes
        )
    
        process {
    #        if ($TemplateString -match '^(?(?.*?)\[(?.*?)\])') {
            if ($TemplateString -match "^(?'stringtoreplace'(?'templatestart'.*?)\[(?'attributename'.*?)\])") {
                if (-not $Attributes.ContainsKey($matches.attributename)) {
                    # You can decide what to do when there's an unknown 
                    # attribute; this will just write a warning and not 
                    # output the string
                    Write-Warning ("Unable to find '{0}' attribute ({1})" -f $matches.attributename, $TemplateString)
                    continue
                }
    
                foreach ($CurrentAttribute in $Attributes[$matches.attributename]) {
                    $TemplateString -replace [regex]::Escape($matches.stringtoreplace), ("{0}{1}" -f $matches.templatestart, $CurrentAttribute) |
                        & $PSCmdlet.MyInvocation.MyCommand -Attributes $Attributes  
                        # Don't let this confuse you. It's just a fancy way of calling 
                        # the function w/o having to use the name (just in case you don't 
                        # like the name I chose)
                }
            }
            else {
                # Return all lowercase string w/o spaces
                $TemplateString.ToLower() -replace '\s'
            }
        }
    }
    

    Then you could call it like this:

    $templates | MakeGroupNames -Attributes $attributes
    
  • #31971
    Profile photo of Curtis Smith
    Curtis Smith
    Participant

    Here is another example using a stack rather than calling a function multiple times. Can also be done this way with a queue. The difference between a stack and a queue is that a queue is first in first out, where as a stack is last in first out.

    Rohn's script will definitely do the trick. Conceptually they are very similar, however, the RegEx had some problems when I tried to run it on my system so that would need some refinement.

    $attributes=@{
                    division=@("US","Europe")
                    department=@("Information Technology","Human Resources","Accounting","Finance")
                    employeeType=@("Employee","Contractor")
                 }
    
    [System.Collections.Stack]$templates = @(
                                                "role.[division].[department]",
                                                "role.contoso.[department]",
                                                "role.contoso.[employeeType]"
                                            )
    
    While ($templates.Count) {
        $item = $templates.Pop()
        $item -match '\[.+?\]' | Out-Null
    #    $item
        $tag = $matches.Values -replace '[\[\]]'
        ForEach ($attrib in $attributes."$tag") {
            $newitem = $item -replace '(.*?)(\[.+?\])(.*)', "`$1$attrib`$3"
            If ($newitem -match '\[.+?\]') {
                $templates.Push($newitem)
            }
            Else {
                ($newitem -replace " ").ToLower()
            }
        }
    }
    

    Results:
    role.contoso.employee
    role.contoso.contractor
    role.contoso.informationtechnology
    role.contoso.humanresources
    role.contoso.accounting
    role.contoso.finance
    role.europe.informationtechnology
    role.europe.humanresources
    role.europe.accounting
    role.europe.finance
    role.us.informationtechnology
    role.us.humanresources
    role.us.accounting
    role.us.finance

  • #31974
    Profile photo of Rohn Edwards
    Rohn Edwards
    Participant

    Whoops! Curtis is right, the regex was wrong in my answer. It looks like the forum ate my angle brackets for my group names. Instead of figuring out how to get them to show up, I'll just change the regex to use the single quotes instead of angle brackets. It should be fixed in my post above now.

    By the way, cool solution, Curtis!

  • #31982
    Profile photo of Curtis Smith
    Curtis Smith
    Participant

    Thanks Rohn, The method of using a function to call itself definitely does work. I have tested your code since you updated the RegEx and all is good. What I like about using a stack or queue is that it is much more efficient. When having a function call itself, new variables and references have to be established each time the function is called that along with every required to initiate the function to begin with make it much slower comparatively. Here are some stats on the two methods.

    Function calling itself:
    Average : 2.2696 milliseconds per run with a 10,000 run sample

    Stack/Queue:
    Average : 0.0191541499999998 milliseconds per run with a 10,000 run sample

    i love working on problems like this. They help me stretch my imagination and allow me to see ingenious solutions submitted by others. I made a slight adjustment to resolve a low potential issue.

    $attributes=@{
                    division=@("US","Europe")
                    department=@("Information Technology","Human Resources","Accounting","Finance")
                    employeeType=@("Employee","Contractor")
                 }
    
    [System.Collections.Stack]$templates = @(
                                                "role.[division].[division].[department]"
                                            )
    
    While ($templates.Count) {
        $item = $templates.Pop()
        $item -match '\[.+?\]' | Out-Null
    #    $item
        $tag = $matches.Values -replace '[\[\]]'
        ForEach ($attrib in $attributes."$tag") {
            $newitem = $item -replace "\[$tag\]", "$attrib"
            If ($newitem -match '\[.+?\]') {
                $templates.Push($newitem)
            }
            Else {
                ($newitem -replace " ").ToLower()
            }
        }
    }
    

    Prior to the adjustment, if a tag were used more than once, it would get undesired results.

    Before the adjustment, "role.[division].[division].[department]" would produce:
    role.europe.europe.informationtechnology
    role.europe.europe.humanresources
    role.europe.europe.accounting
    role.europe.europe.finance
    role.europe.us.informationtechnology
    role.europe.us.humanresources
    role.europe.us.accounting
    role.europe.us.finance
    role.us.europe.informationtechnology
    role.us.europe.humanresources
    role.us.europe.accounting
    role.us.europe.finance
    role.us.us.informationtechnology
    role.us.us.humanresources
    role.us.us.accounting
    role.us.us.finance

    After the adjustment, "role.[division].[division].[department]" will produce:
    role.europe.europe.informationtechnology
    role.europe.europe.humanresources
    role.europe.europe.accounting
    role.europe.europe.finance
    role.us.us.informationtechnology
    role.us.us.humanresources
    role.us.us.accounting
    role.us.us.finance

  • #32010
    Profile photo of Jamie Nelson
    Jamie Nelson
    Participant

    Wow, thanks to both of you guys for your help! Exactly what I was looking for. I pride myself in being above average in PowerShell, but sometimes logic issues like this one give me fits. Definitely learned some new tricks!

    These are both excellent solutions but ultimately I think I will go with Curtis' approach. I timed it as well and it is slightly more efficient. Plus, I'm also wanting to build a dynamic group membership filter along with the group name, and at first glance, it appears that will be a little less complicated in a while loop as opposed to recursively calling a function.

    Thanks again to you both!

You must be logged in to reply to this topic.