PowerShell Regular Expressions: Named Capture Groups

Numbered capture groups like (\d+) work until you modify the regex and the group numbering shifts, breaking every reference to $Matches[2] in your code. PowerShell regex capture groups with named syntax (?<name>...) solve this by letting you access captures by a stable, meaningful name instead of a fragile index. This post covers the named group syntax, accessing captures via $Matches, using named groups in replacements, non-capturing groups, and a practical log-parsing example.
Named Group Syntax (?<name>)
Define a named capture group by wrapping the pattern in (?<name>...). The name must start with a letter and contain only letters, digits, and underscores. Use the -match operator (not -like) to trigger capture:
$logLine = "2026-05-04 09:15:33 ERROR [Database] Connection refused"
$pattern = '^(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2}) (?<level>\w+) \[(?<source>[^\]]+)\] (?<message>.+)$'
if ($logLine -match $pattern) {
Write-Host "Date: $($Matches.date)"
Write-Host "Time: $($Matches.time)"
Write-Host "Level: $($Matches.level)"
Write-Host "Source: $($Matches.source)"
Write-Host "Message: $($Matches.message)"
}
Date: 2026-05-04
Time: 09:15:33
Level: ERROR
Source: Database
Message: Connection refused
Access Named Captures via $Matches
After a successful -match operation, $Matches is populated as a hashtable where each named group is a key. Access values with dot notation or hashtable syntax:
$ipPattern = '^(?<ip>\d{1,3}(?:\.\d{1,3}){3}):(?<port>\d+)$'
$endpoint = "192.168.1.50:8443"
if ($endpoint -match $ipPattern) {
$ip = $Matches.ip # "192.168.1.50"
$port = $Matches.port # "8443"
Write-Host "Host: $ip | Port: $port"
}
# $Matches also contains index 0 (full match) and named groups by both name and ordinal
$Matches[0] # full match: "192.168.1.50:8443"
$Matches['ip'] # "192.168.1.50"
$Matches.port # "8443"
Multiple Named Groups
Parse structured data like CSV lines, HTTP access log entries, or email headers with multiple named groups in a single pattern:
$accessLogLine = '10.1.1.5 - jsmith [04/May/2026:09:15:33 +0000] "GET /api/health HTTP/1.1" 200 1234'
$accessPattern = '^(?<ip>[\d.]+) \S+ (?<user>\S+) \[(?<datetime>[^\]]+)\] "(?<method>\w+) (?<path>\S+) [^"]+" (?<status>\d{3}) (?<bytes>\d+)$'
if ($accessLogLine -match $accessPattern) {
[PSCustomObject]@{
IP = $Matches.ip
User = $Matches.user
DateTime = $Matches.datetime
Method = $Matches.method
Path = $Matches.path
Status = [int]$Matches.status
Bytes = [int]$Matches.bytes
}
}
IP : 10.1.1.5
User : jsmith
DateTime : 04/May/2026:09:15:33 +0000
Method : GET
Path : /api/health
Status : 200
Bytes : 1234
Non-Capturing Groups (?:)
Use (?:...) when you need to group pattern elements for quantifiers or alternation without creating a capture group. This keeps $Matches clean and avoids unnecessary numbered groups:
# (?:\.\d{1,3}){3} groups the repeated octet pattern but does not capture each repetition
$ipPattern = '^(?<ip>\d{1,3}(?:\.\d{1,3}){3})$'
"10.20.30.40" -match $ipPattern | Out-Null
$Matches.ip # "10.20.30.40" — only the full IP captured, not individual octets
Named Groups with -replace
In -replace operations, reference named captures with ${name} syntax in the replacement string:
$filename = "report_20260504_v2.xlsx"
$newName = $filename -replace '(?<base>.+?)_(?<date>\d{8})_(?<version>v\d+)\.(?<ext>\w+)',
'${date}_${base}_${version}.${ext}'
Write-Host $newName
20260504_report_v2.xlsx
Log Parsing Real Example
Parse an entire log file into structured objects using named groups and pipeline processing:
$logFile = "C:\Logs\app.log"
$pattern = '^(?<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (?<level>\w+) \[(?<src>[^\]]+)\] (?<msg>.+)$'
$entries = Get-Content $logFile | Where-Object { $_ -match $pattern } | ForEach-Object {
[void]($_ -match $pattern)
[PSCustomObject]@{
Timestamp = [datetime]$Matches.ts
Level = $Matches.level
Source = $Matches.src
Message = $Matches.msg
}
}
$entries | Where-Object Level -eq 'ERROR' | Group-Object Source |
Select-Object Count, Name | Sort-Object Count -Descending
Common Errors and Fixes
-
$Matches only populated after -match operator, not -replace. The
$Matchesautomatic variable is populated only by the-matchoperator. After a-replaceoperation,$Matchesstill contains the result from the last-match, which can cause confusing bugs. If you need captures from a replacement, perform a-matchfirst. -
Group name must start with a letter. Named group names like
(?<1st>...)or(?<_var>...)are invalid. Names must start with a letter (a-z or A-Z) and contain only letters, digits, and underscores. Use descriptive names likedate,ipAddress, orstatusCode.
Related Cmdlets / See Also
Wrapping Up
Named capture groups make regex patterns self-documenting and refactoring-safe. Use (?<name>...) for every capture you need to reference, (?:...) for structural grouping without capture, and ${name} in replacement strings to rearrange captured content. Once you adopt named groups, you will never want to go back to numbered references.


