More PowerShell Replacement Techniques
In the last article, I discussed and demonstrated a number of replacement techniques in PowerShell. You can use these techniques to replace strings or parts of strings. Which approach you use depends on the context of your PowerShell code or script, and as I pointed out last time, there are a few potential stumbling blocks. Today I want to show you a few more techniques and dive into replacing using a regular expression object or patterns.
Replace Operator Revisited
One advanced way to use the -Replace
operator is with regex patterns. We already know you can do this:
PS C:\> "abc 124 xyz" -replace "\d","*"
abc *** xyz
But you can use named captures in your regex pattern and create a replacement based on the capture. Here’s an example.
Let’s say I have log file data like this:
$logdata = "6/6/2022 foo.dat 3456"
I can construct a regular expression pattern to create named captures.
$pattern = '^(?<date>(\d{1,2}\/)+\d{4})\s(?<name>.*\.\w{3})\s(?<size>\d+)$'
I can use this pattern to get named references to the date, file name, and size. Because I have named captures, I can use the name in my replacement. Express the named capture in the format ${capturename}
.
$replace = '${name} ${size} bytes'
Use single quotes so that PowerShell doesn’t interpret ${name}
as a variable.
PS C:\> $logdata -replace $pattern,$replace
foo.dat 3456 bytes
Using the named captures, I created an entirely new string. I did more than simply replace one string with another. However, for this to work, you need to know your data, and it must be consistent.
Here’s one more example for you to try.
$pattern = '(?<userpath>C:\\Users\\)(?<username>\w+)(?<remainder>(\\.*)+)?'
$replace = '${userpath}JDOE${remainder}'
I’m going to replace the user name in the paths with JDOE.
PS C:\> $env:USERPROFILE -replace $pattern, $replace
C:\Users\JDOE
PS C:\> $env:LocalAppData -replace $pattern, $replace
C:\Users\JDOE\AppData\Local
If you need to learn more about using regular expressions in PowerShell, I have a Pluralsight course on the topic
Replace with Scriptblocks
In PowerShell 7, you can use a scriptblock in your replacement with the -Replace
operator. PowerShell will pass the matching value to the scriptblock. Technically, it is passing a System.Text.RegularExpressions.Match object. The matching text is in the Value
property.
PS C:\> "My name is jeff" -replace "jeff",{$_.value.toUpper()}
My name is JEFF
You can get quite creative. Here’s a demo based on an example in about_Comparison_Operators
.
$q = "I *am* THE walrus!" -replace "\S", { "{0:d3}" -f [int][char]$_.value }
The pattern is looking for any non-whitespace character. Each match is turned into the integer value of the [char] version of the character. I’m using the format operator -f to pad the integer to 3 places with leading zeros.
PS C:\> $q
073 042097109042 084072069 119097108114117115033
Want to try and come up with PowerShell replacement code to reverse it? You should come up with something like this:
PS C:\> $q -replace "\d{3}", { [char][int]$_.value }
I *am* THE walrus!
You could easily turn these into a set of functions for encoding and decoding strings.
I could also use named captures. I’ll re-use the log data example.
$pattern = '^(?<date>(\d{1,2}\/)+\d{4})\s(?<name>.*\.\w{3})\s(?<size>\d+)$'
$replace = { "{0} {1:n2}KB - {2}" -f ($_.groups["name"].value),([int]($_.groups["size"].value)/1kb),($_.groups["date"].value)}
The replacement scriptblock will create a new string, formatting the size as KB to two decimal places.
PS C:\> $logdata = "6/6/2022 foo.dat 3456987"
PS C:\> $logdata -replace $pattern,$replace
foo.dat 3,375.96KB - 6/6/2022
Regex Replace
The regular expression object has a Replace()
method. The -Replace
operator makes it easier to use regular expressions without a regex object. But you never know when using the regex object will be the right solution so let’s take a look.
It is simple to create a regex object using the type accelerator.
[regex]$rx = "Jeff"
The Replace()
method has several overloads.
The input is the text to process.
PS C:\> $me = "Jeff Hicks"
PS C:\> $rx.Replace($me, "Jeffery")
Jeffery Hicks
PowerShell performed a regular expression operation on the string “Jeff Hicks” looking to match “Jeff”, If a match is found, PowerShell will replace it with “Jeffery.”
Let’s look at an alternative to an example from earlier.
[regex]$pattern = '(?<userpath>[Cc]:\\[Uu]sers\\)(?<username>\w+)(?<remainder>(\\.*)+)?'
$u = $pattern.Matches($env:temp).groups.where({ $_.name -eq 'username' }).value
This pattern is a bit more explicit because when using the [regex]
object, matches are case-sensitive. The second line is getting the value of the named capture ‘name’. I can now use this value with the -Replace operator.
PS C:\> $u
Jeff
PS C:\> $env:TEMP -replace $u, "XXXXX"
C:\Users\XXXXX\AppData\Local\Temp
Another way to get the named capture value is with an expression like
$u = $pattern.matches($env:temp).groups[$pattern.groupNumberfromName("username")].value
And if you want to simplify your pattern and ignore case, create the regex object like this:
$pattern = [System.Text.RegularExpressions.Regex]::new('(?<userpath>c:\\users\\)(?<username>\w+)(?<remainder>(\\.*)+)?',"IgnoreCase")
Since we’re on the topic of case, let’s revisit my Active Directory example. I want to make multiple replacements to the output from Get-ADDomain and be case-sensitive. Here’s the result I’m starting with.
$a = Get-ADDomain | Out-String
I’m sure there are several ways you could tackle this problem. I’m going to use CSV data. I’ll skip saving it to a file.
$csv = @"
Pattern,Replacement
"Dom1", "DomainController"
"DOM1", "DOMAINCONTROLLER"
"Company", "Foo"
"COMPANY", "FOO"
"Windows20\d{2}", "Windows20XX"
"\d{4}", "0000"
"@
I’ll treat the pattern as a [regex]
object and replace $a
with the replacement value.
$csv | ConvertFrom-Csv |
ForEach-Object {
Write-Host "Replacing $($_.pattern)" -ForegroundColor yellow
$a = ([regex]$($_.pattern)).Replace($a, $_.replacement)
}
Remember, I had to save the output from Get-ADDomain
as a string so I can’t do much with this other than save it to a file. How about making these replacements in the object itself?
$d = Get-ADDomain
I can’t change properties directly because some of them are read-only. But I can create a copy of the object and replace property values as needed. To do this, I can dig into the abstracted psobject and look at the properties.
PS C:\> $d.psobject.properties | Select-Object Name,Value -first 7
Name Value
---- -----
AllowedDNSSuffixes {}
ChildDomains {}
ComputersContainer CN=Computers,DC=Company,DC=Pri
DeletedObjectsContainer CN=Deleted Objects,DC=Company,DC=Pri
DistinguishedName DC=Company,DC=Pri
DNSRoot Company.Pri
DomainControllersContainer OU=Domain Controllers,DC=Company,DC=Pri
I’m going to create a hashtable and process each property. I’ll replace values as needed and add each property and value to the hashtable. At the end of the process, I can turn the hashtable into an object, and the result will be indistinguishable from the original object. I’m using the same CSV data from above.
$out = $d.psobject.properties |
ForEach-Object -Begin {
$tmp = [ordered]@{}
} -Process {
$propName = $_.name
$propValue = $_.value
# Write-Host "Testing property $propname" -ForegroundColor yellow
#replace each value
$csv | ConvertFrom-Csv |
ForEach-Object {
# Write-Host "comparing $propvalue to pattern $($_.pattern)" -foregroundColor Cyan
$rx = $_.pattern -as [regex]
if ( $rx.IsMatch($propValue)) {
# Write-Host "Replacing $propvalue with $($_.replacement)" -fore green
$propvalue = $rx.Replace($propvalue, $_.replacement)
}
}
$tmp.Add($propname, $propvalue)
} -End {
[pscustomobject]$tmp
}
In essence, I’ve cloned the original object but with replaced property values.
PS C:\> $out | Select-Object DistinguishedName,Name,NetBiosName,PDCEmulator,DomainMode,DNSRoot
DistinguishedName : DC=Foo,DC=Pri
Name : Foo
NetBIOSName : FOO
PDCEmulator : DOMAINCONTROLLER.Foo.Pri
DomainMode : Windows20XXDomain
DNSRoot : Foo.Pri
I thought this was useful enough to turn into a function.
Function Protect-ObjectOutput {
[cmdletbinding()]
[alias("Spoof-Object")]
Param(
[Parameter(Mandatory, ValueFromPipeline)]
[object]$Inputobject,
[Parameter(Mandatory, HelpMessage = "The path to the replacements CSV file.")]
[ValidateScript({ Test-Path $_ })]
[string]$ReplacementPath
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Importing replacement data from $ReplacementPath"
$Replace = Import-Csv -Path $ReplacementPath
} #begin
Process {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] $Inputobject "
$Inputobject.psobject.properties |
ForEach-Object -Begin {
$tmp = [ordered]@{PSTypename = $inputobject.psobject.typenames[0] }
} -Process {
$propName = $_.name
$propValue = $_.value
Write-Verbose "Testing property $propname"
#replace each value
$replace | ForEach-Object {
$rx = $_.pattern -as [regex]
if ( $rx.IsMatch($propValue)) {
Write-Verbose "Replacing $propvalue with $($_.replacement)"
$propvalue = $rx.Replace($propvalue, $_.replacement)
}
}
$tmp.Add($propname, $propvalue)
} -End {
[pscustomobject]$tmp
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
} #close Protect-ObjectOutput
You can use this function with anything. Specify a CSV with your replacements, and PowerShell will do the rest.
PS C:\> Get-ADForest | Protect-ObjectOutput -ReplacementPath c:\work\replace.csv
ApplicationPartitions : DC=DomainDnsZones,DC=Foo,DC=Pri DC=ForestDnsZones,DC=Foo,DC=Pri
CrossForestReferences : {}
DomainNamingMaster : DOMAINCONTROLLER.Foo.Pri
Domains : Foo.Pri
ForestMode : Windows20XXForest
GlobalCatalogs : DOMAINCONTROLLER.Foo.Pri
Name : Foo.Pri
PartitionsContainer : CN=Partitions,CN=Configuration,DC=Foo,DC=Pri
RootDomain : Foo.Pri
SchemaMaster : DOMAINCONTROLLER.Foo.Pri
Sites : {Default-First-Site-Name}
SPNSuffixes : {}
UPNSuffixes : {}
PropertyNames : {ApplicationPartitions, CrossForestReferences, DomainNamingMaster, Domains…}
AddedProperties : {}
RemovedProperties : {}
ModifiedProperties : {PartitionsContainer}
PropertyCount : 13
I even defined a fun alias.
Summary
I hope you found this foray into PowerShell replacing techniques informative. I’d love to hear how you are using replacement in your work. Feel free to leave a comment or question.