The other day I'm chatting with my friend Gladys Kravitz about Active Directory and she makes an off-hand remark about parsing out organizational unit names from a distinguished name. On one hand, this is a pretty simple task, assuming a proper distinguished name from the Active Directory cmdlets. All you really need to do is split the string.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
$ou = "OU=Foo,OU=Top,DC=Company,DC=pri"
$ou.split(",")[0]
The code is splitting on the comma and selecting the first element in the array.
But I thought there's like an alternative. And what if I want just the name of the top level OU, i.e. Foo? So naturally I turned to regular expressions.
Don't Be Greedy
I began by creating a set of test distinguished name values.
$DN = @("OU=Foo,OU=Top,DC=Company,DC=pri",
"OU=Foo123,OU=FooBar,DC=Company,DC=pri",
"ou=Foo,ou=Top,dc=Company,dc=pri",
"OU=Gladys Kravitz Monitors,OU=Top,DC=Company,DC=pri",
"OU=A723-Test,OU=Top,OU=Middle,DC=Company,DC=pri",
"OU=JEA_Operators,DC=Company,DC=Pri",
"CN=Users,DC=Company,DC=pri",
"CN=ArtD,OU=IT,DC=Company,DC=Pri",
"cn=johnd,ou=sales,dc=company,dc=pri",
"CN=SamS,OU=This,OU=That,DC=Company,DC=Pri",
"OU=Domain Controllers,DC=Company,DC=Pri")
If you look closely, you'll see I am using a variety of forms since I may not always be able to guarantee what the data will look like. I'm also starting this exercise from the perspective of getting the top OU name from an OU distinguished name. I have included test values that I know should fail. When developing with a regex pattern it is important to test with data you know should fail.
With this data I started with this regular expression pattern:
[regex]$rx = "^(ou|OU)=.*(?=,)"
The pattern says, "Anchor at the start of line line (^) and look for the string "ou" or (|) "OU" followed by =. Then any characters (.*) where if you look ahead you see a comma (?=,)." Let's try with the first element in the array.
This mostly worked. The problem is that the pattern matched on every possible string. In other words, the regular expression pattern was greedy. Most of the time this isn't a problem. But, here I want the regex pattern to stop after the first OU match.
[regex]$rx = "^(ou|OU)=.*?(?=,)"
The main difference is the "?" inserted after ."*". This makes the pattern stop after the first match.
Now I can process array of names.
Not as clean as I'd like since I'm getting blanks for the strings that don't match. This might be better.
Now I'm only getting the value if there is a match. But I want to take this even further.
Named Captures
I've decided I want get the name of the first OU, such as Foo and Foo123. To do this I'll use a named capture. Here's my revised regex pattern.
[regex]$rx = "^(ou|OU)=(?<OUName>.*?(?=,))"
The main difference is that I am now defining a name, OUName, for anything that matches in the last part of the pattern which is wrapped in parentheses. Adding "?<OUName>" in front of the non-greedy pattern, defines the name. Matching now includes a new group.
Here's a scripting trick to get just named capture.
Which means I can process my array:
$dn | Where-Object { $rx.IsMatch($_) } | ForEach-Object { $rx.Match($_).groups["OUName"].Value }
My next step would normally be to build a function based on this code. But I'm not finished.
Get Top OU Name
What about distinguished names for users and groups? I want to be able to parse out the name of the top level organizational unit. I'll test with this.
$art = "CN=ArtD,OU=IT,DC=Company,DC=Pri"
The tricky part is that the string might start with CN or OU. Here's yet another version of my regex pattern.
[regex]$rx = "^(((CN=.*?))?)(ou|OU)=(?<OUName>.*?(?=,))"
With this pattern I'm looking for something that might start with
"CN=something" at the beginning.
^(((CN=.*?))?)
The pattern is using a non-greedy match. The overall pattern is optional. That is, it might exist. The format if "( <your-pattern>?)". Let's see if it works.
Looks good. Although if the string started with "cn", it will fail.
This is one potential pitfall when working with regular expressions in PowerShell and why I added a variety of casings in my $DN test data set. A better way to avoid case issues is to define the regex object like this.
$rx = [System.Text.RegularExpressions.Regex]::new("^(((CN=.*?))?)OU=(?<OUName>.*?(?=,))","IgnoreCase")
Now it won't matter if the string uses OU, ou or oU. They will all match.
And using my test data now processes the user and group names.
Excellent. I'll build a function based on this code that will process a distinguished name and provide the name of the first or top organizational unit name.
Function Get-TopOUName {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[alias("dn")]
[string]$DistinguishedName
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
#ignore case
$rx = [System.Text.RegularExpressions.Regex]::new("^(((CN=.*?))?)OU=(?<OUName>.*?(?=,))","IgnoreCase")
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $DistinguishedName"
If ($rx.IsMatch($DistinguishedName) ) {
$rx.Match($DistinguishedName).groups["OUName"].Value
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
If you look closely, you'll see that CN=Users,DC=Company,DC=pri didn't produce a result which means my pattern is working. Let's test this with Active Directory.
Get-ADUser -filter * | Select-Object Name,SamAccountName,
@{Name="OU";Expression={Get-TopOUName $_.distinguishedname}},distinguishedname
Those first few accounts aren't in an OU so the result is as expected. But now I have a tool I can use.
Learn More
You may not have a practical need for my patterns or codes to work with Active Directory. But I hope you'll learn from how I approached the problem and illustrated a few regular expression concepts. If you want to know more about regular expressions, I have an entire Pluralsight course on the subject.
If simple string splitting works, by all means take the easy route. But sometimes you might needs something a bit more sophisticated and I think you'll find regular expressions to be an elegant solution.
This is the way I have done it in the past
function Get-NameFromDN {
param
(
[System.String]
$DN
)
return ($DN.replace(‘\’, ”) -split ‘,*..=’)[2]
}
There are no shortage of techniques.
To be clear, this post, as most of my work, is intended to be educational and not a production-ready solution. In fact, as written, if the OU name includes a comma, my code won’t work as expected. I’d have to revise the regex pattern.
Hi Jeffery, another approach would be this:
$ou = “OU=Foo,OU=Top,DC=Company,DC=pri”
$ou.split(“,”)[0] -replace “ou=” -replace “cn=”
It seems simpler to me and using the replace switches we get rid of dreadful regex:)
For this particular use case, sure that might be better. And I’m not implying that my code is the best way. I’m merely using it as use-case to teach about regular expressions when you that *is* what you want to use. I also get your point about the dreadful nature of regular expressions. I use to dread them until I fully understood them. Now that I “speak” regex, I find them easier to use than trying to split and parse strings.