Regular expressions (regex) are patterns used to match, search, and manipulate text. PowerShell has built-in support for regex through operators like -match, -replace, and -split, as well as the .NET [regex] class.
Basic Examples
Testing a match
The -match operator returns $true or $false depending on whether the pattern is found in the string.
$email = "[email protected]"
if ($email -match "@") {
Write-Host "Contains an @"
}
$result = "PowerShell 7" -match "\d"
$result
Result:
Contains an @
True
Extracting a match with $Matches
When -match succeeds, PowerShell populates the automatic variable $Matches with the matched value and any capture groups.
$version = "Version 7.4.1"
if ($version -match "(\d+\.\d+\.\d+)") {
Write-Host "Found version: $($Matches[1])"
}
Result:
Found version: 7.4.1
Replacing text with -replace
The -replace operator takes a pattern and a replacement string.
$text = "Hello, my name is John Doe."
$result = $text -replace "John Doe", "Jane Smith"
$result
Result:
Hello, my name is Jane Smith.
You can also use a regex pattern in the replacement to strip unwanted characters:
$dirty = "Ph0ne: +31 (0)6-12 34 56 78"
$clean = $dirty -replace "[^0-9+]", ""
$clean
Result:
+31061234567
Splitting a string with -split
The -split operator accepts a regex pattern as the delimiter.
$csv = "one,two,,three, four"
$parts = $csv -split ",\s*"
$parts
Result:
one
two
three
four
Searching in files with Select-String
Select-String scans string input or files and returns lines that match a pattern.
$lines = @(
"ERROR: disk full",
"INFO: backup started",
"WARNING: low memory",
"ERROR: connection lost"
)
$lines | Select-String -Pattern "^ERROR"
Result:
ERROR: disk full
ERROR: connection lost
Advanced Examples
Named capture groups
Named groups make patterns easier to read and maintain. Use (?<name>...) syntax to define a named group, then reference it via $Matches.name.
$logLine = "2024-06-19 14:32:01 ERROR Something went wrong"
if ($logLine -match "(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>\w+) (?<message>.+)") {
Write-Host "Date : $($Matches.date)"
Write-Host "Time : $($Matches.time)"
Write-Host "Level : $($Matches.level)"
Write-Host "Message: $($Matches.message)"
}
Result:
Date : 2024-06-19
Time : 14:32:01
Level : ERROR
Message: Something went wrong
Finding all matches with [regex]::Matches()
Unlike -match, which stops at the first match, [regex]::Matches() returns every match in the input.
$text = "Call us at 555-1234 or 555-5678 for support."
$matches = [regex]::Matches($text, "\d{3}-\d{4}")
foreach ($m in $matches) {
Write-Host "Found number: $($m.Value)"
}
Result:
Found number: 555-1234
Found number: 555-5678
Using [regex]::Replace() with named groups
The .NET [regex]::Replace() method lets you restructure text using capture groups in the replacement string. This example evolves through three steps: positional groups, named groups, and what happens when you make a typo.
Step 1 - Positional groups
The simplest approach uses numbered positional references ($1, $2, …) to refer to capture groups.
$dates = '29 July, 2014, 30 July, 2014, 31 July, 2014'
[regex]::Replace($dates, '\s*([^,]+)(,\s*)([^,]+),*\s*', '$1 = $3' + "`n")
Result:
29 July = 2014
30 July = 2014
31 July = 2014
This works, but $1, $2, $3 tell you nothing about what each group captures. As patterns grow more complex, positional references become hard to follow.
Step 2 - Named groups
Replace the numbered groups with named groups using (?<name>...) syntax. You then reference them as ${name} in the replacement string.
$dates = '29 July, 2014, 30 July, 2014, 31 July, 2014'
[regex]::Replace($dates, '\s*(?<day>[^,]+)(?<sep>,\s*)(?<year>[^,]+),*\s*', '${day} = ${year}' + "`n")
Result:
29 July = 2014
30 July = 2014
31 July = 2014
The pattern and replacement are now self-documenting: ${day} and ${year} make the intent obvious.
Step 3 - What happens with a typo?
If you mistype a group name in the replacement string, .NET does not throw an error. Instead it outputs the literal text of the bad reference - making typos immediately visible in the output.
$dates = '29 July, 2014, 30 July, 2014, 31 July, 2014'
# Note the typo: ${yr} instead of ${year}
[regex]::Replace($dates, '\s*(?<day>[^,]+)(?<sep>,\s*)(?<year>[^,]+),*\s*', '${day} = ${yr}' + "`n")
Result:
29 July = ${yr}
30 July = ${yr}
31 July = ${yr}
The replacement token ${yr} appears verbatim in every line, making the mistake easy to spot - far better than silently producing wrong output.
Validating an email address
A common use case for regex is input validation. The pattern below covers most standard email formats.
function Test-EmailAddress {
param([string]$Address)
$pattern = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if ($Address -match $pattern) {
Write-Host "'$Address' is a valid email address"
} else {
Write-Host "'$Address' is NOT a valid email address"
}
}
Test-EmailAddress "[email protected]"
Test-EmailAddress "not-an-email"
Test-EmailAddress "missing@tld."
Result:
'[email protected]' is a valid email address
'not-an-email' is NOT a valid email address
'missing@tld.' is NOT a valid email address
Parsing structured log lines into objects
Combining regex with [PSCustomObject] lets you turn raw log data into structured objects that can be filtered, sorted, and exported.
$logLines = @(
"2024-06-19 08:01:44 INFO Service started",
"2024-06-19 08:15:22 ERROR Disk usage above 90%",
"2024-06-19 08:17:05 WARN Memory pressure detected",
"2024-06-19 08:20:11 ERROR Connection timeout on port 443"
)
$pattern = '^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>\w+)\s+(?<message>.+)$'
$entries = foreach ($line in $logLines) {
if ($line -match $pattern) {
[PSCustomObject]@{
Date = $Matches.date
Time = $Matches.time
Level = $Matches.level
Message = $Matches.message
}
}
}
# Show only errors
$entries | Where-Object { $_.Level -eq "ERROR" } | Format-Table -AutoSize
Result:
Date Time Level Message
---- ---- ----- -------
2024-06-19 08:15:22 ERROR Disk usage above 90%
2024-06-19 08:20:11 ERROR Connection timeout on port 443