Dynamically build group names with a hashtable and string array

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

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.

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

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

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

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!

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

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!