Here is my issue. If I type $env:UserName, the output is JoeDow
What I need is to trim this string from the end until the first capital character.
The output should be JoeD
I tried everything and had no luck
Thanks in advance
M
RegEx is amazing, but simple in this case:
$String = 'JoeDow'
$String -creplace '[a-z]+$'
This simply says case sensitively replace any lowercase character from a-z that's at the end of the string.
In your case:
$env:USERNAME -creplace '[a-z]+$'
Note: There are lots of ways I can think of doing this. The particular approach above assumes there's 2 Capital letters and we want to chop only the lowercase letters after the last capital letter. If you give it a string like 'Joe' it will return "J", because it'll replace the trailing lowercase letters just the same. Therefore if this is not a super-consistent scenario we'd likely have to go another route.
Update, Building on Sage's nice example
If you use .LastIndexOfAny(), again with assurance the format is stable, you can nail it in very few lines:
$String = "JoeDow"
$CapitalLetters = [Char[]](65..90)
$String.Substring(0, ($String.LastIndexOfAny($CapitalLetters) +1 ))
This relies on the last capital not the second capital. Again, it depends on how reliable the pattern your expecting is.
Another RegEx Approach:
$String = "JoeDow"
$String = -join ($String -csplit "([A-Z])")[1..3]
$String
This one uses capital letters as the delimiter, however the parens instructs the -csplit operator to return the delimiter in the array. So, by picking the correct range we can be sure to grab "J" + "oe" + "D". Unlike the other examples this uses the 2nd capital, not necessarily the last. While the other chose the last which was not necessarily the 2nd.
You can easily get an array of all the capital letters in Powershell with [char[]](65..90)
From there, using .IndexOfAny method, you can gather the position of the second capital letter and use the substring method to select the string from its beginning to the second capital letter exclusively.
That would look like this.
Function Get-TrimmedName($Name) {
$CapitalLetters = [char[]](65..90)
$SecondCapEndIndex = $Name.IndexOfAny($CapitalLetters, 1) + 1
$TrimmedName = $Name
if ($SecondCapEndIndex -gt 0 -and $SecondCapEndIndex -lt $Name.Length) {
$TrimmedName = $Name.Substring(0, [int]$SecondCapEndIndex)
}
return $TrimmedName
}
Get-TrimmedName -Name 'JoeDow' # returns JoeD
Get-TrimmedName -Name 'MaxD' # returns MaxD
Get-TrimmedName -Name 'Max' # returns Max
Get-TrimmedName -Name 'PatrickDesjardins' # returns PatrickD
Now, just because fun needed to be had, here is a more complex version that will go futher and work with more complex name such as JohnDoeDawson (Would return JohnDD), allow you to get initials out of it (eg: JohnDoeDawson = JDD) and even convert names to Title cases on the fly (eg: "john doe dawson" = JohnDD)
<#
.SYNOPSIS
Get the trimmed version of a name.
.DESCRIPTION
Returns the complete first name and the trimmed down last names (First letter only)
.PARAMETER Name
Name to be trimmed.
.PARAMETER Initials
If set, only the initials will be returned.
.PARAMETER TitleCase
If set, the name passed down will be converted to TitleCase first. It is good to be noted that names with no
spaces will loose any capitals after the First letter (eg: JohnDoe -> Johndoe) so it is best used when the name is formatted
with spaces.)
.EXAMPLE
An example
.NOTES
General notes
#>
Function Get-TrimmedName([String]$Name, [Switch]$Initials, [Switch]$TitleCase) {
$CapitalLetters = [char[]](65..90)
$Index = 0
$output = [System.Text.StringBuilder]::new()
if ($TitleCase) {
$Ti = [cultureinfo]::CurrentCulture.TextInfo
$Name = $Ti.ToTitleCase($Name)
}
$MyCapLetters =
while ($true) {
$Index = $Name.IndexOfAny($CapitalLetters, $Index)
if ($Index -eq -1) { break }
$Index
$Index += 1
}
if ($Initials) {
if ($MyCapLetters.Count -eq 0) {
return $Name[0]
}
Foreach ($C in $MyCapLetters) { $Output.Append($Name.Substring($C, 1)) | Out-Null }
return $Output.ToString()
}
$MyCapLetters = $MyCapLetters.Where( { $_ -gt 0 })
if ($MyCapLetters.Count -gt 0) {
$output.Append($Name.Substring(0, $MyCapLetters[0] + 1)) | Out-Null
}
else {
return $Name
}
foreach ($C in $MyCapLetters | Select-Object -Skip 1) {
$output.Append($Name.Substring($C, 1)) | Out-Null
}
return $output.ToString()
}
Now some examples of this more complex function
#Base example
Get-TrimmedName -Name JohnDoe # Returns JohnD
# Works with complex names
get-trimmedname 'PatrickDesjardinsPowell' # returns PatrickDP
#Initials only
get-trimmedname 'PatrickDesjardinsPowell' -Initials # Returns PDP
#TitleCase - if the name contains spaces
get-trimmedname -name "John doe" -TitleCase #Returns JohnD
get-trimmedname -name "john doe" -TitleCase #Returns JohnD
# Do not use titlecase if the name is in 1 word
# TitleCase behavior is to find word boundaries so "jOHNDoe" would get converted to Johndoe since
# considered as a single word.
get-trimmedname -name "JohnDoe" -titlecase #Returns Johndoe
References
MSDN - .Net String.IndexOfAny Method
MSDN - .Net String.Substring Medhod
MSDN - Everything about arrays - Powershell - Special Index tricks
OK, I need to add some more information. I'm going to run this script on the remote machine via Intune. The problem is if I put a full email address in the script - all good, but if I use a variable like $env:username it pickup system credentials, and the script failed. Is any way to get a username on the remote machine using Intune? It will be executed on a couple of computers. Thanks
Related
I have a PS script that checks some custom user's properties in Active Directory.
One of the properties is "Manager".
$data = Get-ADUser $user -Properties * | Select-Object DisplayName, LockedOut, Enabled, LastLogonDate, PasswordExpired, EmailAddress, Company, Title, Manager, Office
Write-Host "9." $user "manager is" $data.manager -ForegroundColor Green
When I run the script I've got:
User's manager is CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com
The problem is that text "OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com" will be different for some users
How can I modify output and remove everything except "cool.boss"?
Thank you in advance
This should be a more or less safe and still easy way to parse it:
($data.manager -split "," | ConvertFrom-StringData).CN
To complement the helpful answers here with PowerShell-idiomatic regex solutions:
Using -split, the regex-based string splitting operator:
$dn = 'CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com'
($dn -split '(?:^|,)CN=|,')[1] # -> 'cool.boss'
Using -replace, the regex-based string substitution operator:
$dn = 'CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com'
$dn -replace '(?:^|,)CN=([^,]+).*', '$1' # -> 'cool.boss'
Note:
The above solutions do not rely on a specific order of the name-value pairs (RDNs) in the input (that is, a CN entry needn't be the first one), but they do extract only the first CN entry's value, should multiple ones be present, and they do assume that (at least) one is present.
In principle, DNs (Distinguished Names), of which the input string is an example, can have , characters embedded in the values of the name-value pairs that make up a DN, escaped as \, (or, in hex notation, \2C); e.g., "CN=boss\, cool,OU=Users,..."
A truly robust solution would have to take that into account, and would ideally also unescape the resulting value; none of the existing answers do that as of this writing; see below.
Robustly parsing an LDAP/AD DN (Distinguished Name):
The following Split-DN function:
handles escaped, embedded , chars., as well as other escape sequences, correctly
unescapes the values, which includes not just removing syntactic \, but also converting escape sequences in the form \<hh>, where hh is a two-digit hex. number representing a character's code point, to the actual character they represent (e.g, \3C, is converted to a < character).
outputs an ordered hashtable whose keys are the name components (e.g., CN, OU), with the values for names that occur multiple times - such as OU - represented as an array.
Example call:
PS> Split-DN 'CN=I \3C3 Huckabees\, I do,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC=com'
Name Value
---- -----
CN I <3 Huckabees, I do
OU {Users, SO, PL, RET…}
DC {domain, com}
Note how escape sequence \3C was converted to <, the character it represents, and how \, was recognized as an , embedded in the CN value.
Since the input string contained multiple OU and DC name-value pairs (so-called RDNs, relative distinguished names), their corresponding hashtable entries became arrays of values (signified in the truncated-for-display-only output with { ... }, with , separating the elements).
Function Split-DN's source code:
Note: For brevity, error handling and validation are omitted.
function Split-DN {
param(
[Parameter(Mandatory)]
[string] $DN
)
# Initialize the (ordered) output hashtable.
$oht = [ordered] #{}
# Split into name-value pairs, while correctly recognizing escaped, embedded
# commas.
$nameValuePairs = $DN -split '(?<=(?:^|[^\\])(?:\\\\)*),'
$nameValuePairs.ForEach({
# Split into name and value.
# Note: Names aren't permitted to contain escaped chars.
$name, $value = ($_ -split '=', 2).Trim()
# Unescape the value, if necessary.
if ($value -and $value.Contains('\')) {
$value = [regex]::Replace($value, '(?i)\\(?:[0-9a-f]){2}|\\.', {
$char = $args[0].ToString().Substring(1)
if ($char.Length -eq 1) { # A \<literal-char> sequence.
$char # Output the character itself, without the preceding "\"
}
else { # A \<hh> escape sequence, conver the hex. code point to a char.
[char] [uint16]::Parse($char, 'AllowHexSpecifier')
}
})
}
# Add an entry to the output hashtable. If one already exists for the name,
# convert the existing value to an array, if necessary, and append the new value.
if ($existingEntry = $oht[$name]) {
$oht[$name] = ([array] $existingEntry) + $value
}
else {
$oht[$name] = $value
}
})
# Output the hashtable.
$oht
}
You can use the .split() method to get what you want.
$DN = "CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC =com"
$DN.Split(',').Split('=')[1]
What i'd recommend, is throwing it into another Get-ADUser to get the displayname for neater output(:
you could use regex for that:
$s = "CN=cool.boss,OU=Users,OU=SO,OU=PL,OU=RET,OU=HBG,DC=domain,DC =com"
$pattern = [regex]"CN=.*?OU"
$r = $pattern.Replace($s, "CN=OU")
$r
Our naming convention consists of the first name, insertion, and lastname, all separated by dots. An example:
Stack Overflow = Stack.Overflow
Stack over Flow = Stack.over.flow
These outputs will be used later on in the script for the creation of a mailbox, user account, etc.
I've successfully combined the values of all strings by simply plus-ing them together, like this:
$Convention = $Firstname+"."+$Insertion+"."+$LastName
The values for these strings come from information being put in when the stript runs (Read-Host "....")
Now, I'm struggling with making this more dynamic. Of course, not every person has an insertion in their name. Using the given example, the current output of $Convention would be "Stack..Overflow", instead of "Stack.Overflow".
My question to you is: how can I filter out both, the $Insertion and the extra dot, when $Insertion is empty? It's most likely something very simple, but I can't seem to figure out what it is.
Thanks in advance for any given help!
Kr,
Robbert
I would do
$Convention = ('{0}.{1}.{2}' -f $Firstname, $Insertion, $LastName) -replace '\.+', '.'
The -replace uses regex in the first parameter, so '\.+', '.' means to replace 1 or more consecutive dots by a single dot.
Alternatively you could use regex \.{2,} which reads two or more consecutive dots
Example:
$Firstname = 'Robbert'
$Insertion = ''
$LastName = 'Verwaart'
$Convention = ('{0}.{1}.{2}' -f $Firstname, $Insertion, $LastName) -replace '\.+', '.'
Output:
Robbert.Verwaart
The code below will go through each of your $convention array, if this is an array, and test if the insertion is empty. If the $Insertion variable is empty, the $i will remove the $Insertion variable and the extra .. You need to add this into the script as a test, before creating the mailboxes.
foreach ($i in $convention){
if($insertion -eq "" -or $insertion -eq $null) {
$i= $Firstname+"."+$LastName
} else {
continue
}
}
Similar like this - Extract email:password
However we have here the situation that in some files there is other data between the data I want to parse, as example:
email:lastname:firstname:password or email:lastname:firstname:dob:password
So my question is - with which command would I be able to ignore 2 segments like "lastname:firstname" or even 3 parts "lastname:firstname:dob". I am using the below regex to retrieve email:password from a big list.
$sw = [System.IO.StreamWriter]::new("$PWD/out.txt")
switch -regex -file in.txt {
'(?<=:)[^#:]+#[^:]+:.*' { $sw.WriteLine($Matches[0]) }
}
$sw.Close()
You need to refine your regex:
# Create sample input file
#'
...:foo#example.org:password1
...:bar#example.org:lastname:firstname:password2
...:baz#example.org:lastname:firstname:dob:password3
'# > in.txt
# Process the file line by line.
switch -regex -file in.txt {
'(?<=:)([^#:]+#[^:]+)(?:.*):(.*)' { '{0}:{1}' -f $Matches[1], $Matches[2] }
}
For brevity, saving the output to a file was omitted above, so the email-password pairs extracted print to the screen by default, namely as:
foo#example.org:password1
bar#example.org:password2
baz#example.org:password3
Explanation of the regex:
(?<=:) is a positive lookbehind assertion for ensuring that matching starts right after a : character.
Note: I based this requirement on your original question and its sample data.
([^#:]+#[^:]+) uses a capture group (capturing subexpression, (...)) to match an email address up to but not including the next :.
(?:.*): uses a non-capturing subexpression ((?:...)) that matches zero or more characters (.*) unconditionally followed by a :
(.*) uses a capture group to capture all remaining characters after what is effectively the last : on each line, assumed to be the password.
$Matches[1] and $Matches[2] refer to the 1st and 2nd capture-group matches, i.e. the email address and the password.
Assuming you had data like this:
"lastname:firstname"
"lastname:firstname:dob"
"lastname:firstname:password:somepassword"
"lastname:john:firstname:jacob:password:dingleheimershmit
You can move through each row like this:
$items = gc .\stack.txt
ForEach($item in $items){
}
Then we can split each row on a : character and check each of those to see if its a match for the string passwrod. If it is, then we check the next token in the row which should be a password.
This code will get you going, you'll just need to do something meaningful with $password.
$items = gc .\stack.txt
ForEach($item in $items){
"processing $item"
$tokens = $item.Split(":")
For($x=0; $x -lt $tokens.Count;$x++){
$token = $tokens[$x]
#"Checking if $token is like password"
if ($token -match "password"){
"since this token is like password, checking next token which should be a password"
$password = $tokens[$x+1]
Write-Host -ForegroundColor Yellow $password
}
}
}
Question:
The intention of my script is to filter out the name and phone number from both text files and add them into a hash table with the name being the key and the phone number being the value.
The problem I am facing is
$name = $_.Current is returning $null, as a result of which my hash is not getting populated.
Can someone tell me what the issue is?
Contents of File1.txt:
Lori
234 east 2nd street
Raleigh nc 12345
9199617621
lori#hotmail.com
=================
Contents of File2.txt:
Robert
2531 10th Avenue
Seattle WA 93413
2068869421
robert#hotmail.com
Sample Code:
$hash = #{}
Switch -regex (Get-content -Path C:\Users\svats\Desktop\Fil*.txt)
{
'^[a-z]+$' { $name = $_.current}
'^\d{10}' {
$phone = $_.current
$hash.Add($name,$phone)
$name=$phone=$null
}
default
{
write-host "Nothing matched"
}
}
$hash
Remove the current property reference from $_:
$hash = #{}
Switch -regex (Get-content -Path C:\Users\svats\Desktop\Fil*.txt)
{
'^[a-z]+$' {
$name = $_
}
'^\d{10}' {
$phone = $_
$hash.Add($name, $phone)
$name = $phone = $null
}
default {
Write-Host "Nothing matched"
}
}
$hash
Mathias R. Jessen's helpful answer explains your problem and offers an effective solution:
it is automatic variable $_ / $PSItem itself that contains the current input object (whatever its type is - what properties $_ / $PSItem has therefore depends on the input object's specific type).
Aside from that, there's potential for making the code both less verbose and more efficient:
# Initialize the output hashtable.
$hash = #{}
# Create the regex that will be used on each input file's content.
# (?...) sets options: i ... case-insensitive; m ... ^ and $ match
# the beginning and end of every *line*.
$re = [regex] '(?im)^([a-z]+|\d{10})$'
# Loop over each input file's content (as a whole, thanks to -Raw).
Get-Content -Raw File*.txt | foreach {
# Look for name and phone number.
$matchColl = $re.Matches($_)
if ($matchColl.Count -eq 2) { # Both found, add hashtable entry.
$hash.Add($matchColl.Value[0], $matchColl.Value[1])
} else {
Write-Host "Nothing matched."
}
}
# Output the resulting hashtable.
$hash
A note on the construction of the .NET [System.Text.RegularExpressions.Regex] object (or [regex] for short), [regex] '(?im)^([a-z]+|\d{10})$':
Embedding matching options IgnoreCase and Multiline as inline options i and m directly in the regex string ((?im) is convenient, in that it allows using simple cast syntax ([regex] ...) to construct the regular-expression .NET object.
However, this syntax may be obscure and, furthermore, not all matching options are available in inline form, so here's the more verbose, but easier-to-read equivalent:
$re = New-Object regex -ArgumentList '^([a-z]+|\d{10})$', 'IgnoreCase, Multiline'
Note that the two options must be specified comma-separated, as a single string, which PowerShell translates into the bit-OR-ed values of the corresponding enumeration values.
other solution, use convertfrom-string
$template=#'
{name*:Lori}
{street:234 east 2nd street}
{city:Raleigh nc 12345}
{phone:9199617621}
{mail:lori#hotmail.com}
{name*:Robert}
{street:2531 10th Avenue}
{city:Seattle WA 93413}
{phone:2068869421}
{mail:robert#hotmail.com}
{name*:Robert}
{street:2531 Avenue}
{city:Seattle WA 93413}
{phone:2068869421}
{mail:robert#hotmail.com}
'#
Get-Content -Path "c:\temp\file*.txt" | ConvertFrom-String -TemplateContent $template | select name, phone
there is a sample data file
Session 1: {21AD8B68-2A42-459e-BD29-F082F47E71B2}
Started: 06-24-2015 11:00
NDS Tree: TEST_TREE
AD Server: dc01.adatum.com
O=BRANCH/OU=BRANCH_CITY1/CN=user1
User
CN=user1,OU=BRANCH_CITY1,OU=ADATUM,DC=adatum,DC=com
user
O=BRANCH/OU=BRANCH_CITY1/CN=EVERYONE1
Group
CN=EVERYONE1,OU=BRANCH_CITY1,OU=ADATUM,DC=adatum,DC=com
group
O=BRANCH/OU=BRANCH_CITY2/CN=user2
User
CN=user2,OU=BRANCH_CITY2,OU=ADATUM,DC=adatum,DC=com
user
O=BRANCH/OU=BRANCH_CITY2/CN=EVERYONE2
Group
CN=EVERYONE2,OU=BRANCH_CITY2,OU=ADATUM,DC=adatum,DC=com
group
I would like to find a line that contains a string "group" (case sensitive) or "user" (case sensitive). If there will be a match, a line before should be changed like this:
if "user" change a line before to CN=<...>,OU=ADATUM,DC=adatum,DC=com
if "group" change a line before to CN=<...>,OU=GROUPS,OU=ADATUM,DC=adatum,DC=com
Of course, an output is a data file that contains all changes.
Any idea?
Many thanks in advance,
M.
The easiest way to accomplish this is probably by using a regular for loop to keep track of line numbers - if line $n matches "user", replace the string in line $n-1.
To do a case-sensitive regex, use -cmatch (notice the c prefix). In the example below I've used a named capture group ((?<name>pattern)) to match and capture either user or group.
The last part, adding a new path to the existing CN=<...> part can be accomplished with the -split command and a lookbehind to avoid messing up escaped commas in the CN value:
# Read file, line by line
$SampleFile = #(Get-Content C:\path\to\data.txt)
# Loop over the text by line numbers
for($i=0;$i -lt $SampleFile.Count;$i++){
# Test if the line matches
if(![string]::IsNullOrWhiteSpace($SampleFile[$i]) -and $SampleFile[$i].Trim() -cmatch "(?<type>^group|user$)"){
# If so, use the match to determine the DN suffix
switch($Matches["type"]){
"group" { $SampleFile[$i-1] = "{0},OU=GROUPS,OU=ADATUM,DC=adatum,DC=com" -f ($SampleFile[$i-1] -split "(?<!\\),")[0] }
"user" { $SampleFile[$i-1] = "{0},OU=ADATUM,DC=adatum,DC=com" -f ($SampleFile[$i-1] -split "(?<!\\),")[0] }
}
}
}
$SampleFile | Out-File C:\path\to\output.txt -Force
Something like this should do the trick:
$c = Get-Content .\file_name.txt
for ($i = 0; $i -lt $c.length; $i++) {
if ($c[$i] -cmatch "^group" ) {
$c[$i-1] = "CN=<...>,OU=ADATUM,DC=adatum,DC=com"
}
elseif ($c[$i] -cmatch "^user") {
$c[$i-1] = "CN=<...>,OU=GROUPS,OU=ADATUM,DC=adatum,DC=com"
}
}
$c | Out-File .\new_file.txt