To extract text between two delimiters in PowerShell, match it with a regular expression and a capture group. The part in parentheses lands in $Matches[1]:
$text = 'Invoice [1042] shipped to [Austin, TX]'
if ($text -match '\[(.*?)\]') {
"Between the first brackets: $($Matches[1])"
}
Output:
Between the first brackets: 1042
The ? after .* matters more than it looks, and -match only returns the first match. Both are covered below. I ran every example in PowerShell 7.6 and Windows PowerShell 5.1, with the same results.
Greedy vs lazy: why you got too much text
.* grabs as much as it can, so it runs from the first bracket to the last one. .*? stops at the first closing bracket:
$text = 'Invoice [1042] shipped to [Austin, TX]'
if ($text -match '\[(.*)\]') { "Greedy (.*): $($Matches[1])" }
if ($text -match '\[(.*?)\]') { "Lazy (.*?): $($Matches[1])" }
Output:
Greedy (.*): 1042] shipped to [Austin, TX
Lazy (.*?): 1042

Another safe pattern is “anything except the closing delimiter,” like \[([^\]]*)\]. Brackets and parentheses need a backslash in regex because they have special meanings.
Get every match, not just the first
-match stops at the first match. [regex]::Matches() returns all of them, and Groups[1] is the captured text:
$text = 'Invoice [1042] shipped to [Austin, TX]'
[regex]::Matches($text, '\[(.*?)\]') | ForEach-Object { $_.Groups[1].Value }
Output:
1042
Austin, TX
Extract text between parentheses
The same idea works for parentheses. Here it pulls the area code out of US phone numbers:
$phones = '(212) 555-0147', '(415) 555-0199', '555-0123'
foreach ($phone in $phones) {
if ($phone -match '\((\d{3})\)') { "$phone -> area code $($Matches[1])" }
else { "$phone -> no area code" }
}
Output:
(212) 555-0147 -> area code 212
(415) 555-0199 -> area code 415
555-0123 -> no area code
Watch out: $Matches keeps its old value
When -match fails, it doesn’t clear $Matches. The previous result is still there:
foreach ($text in 'Order (1042)', 'No parentheses here') {
$null = $text -match '\((\d+)\)'
"$text -> $($Matches[1])"
}
Output:
Order (1042) -> 1042
No parentheses here -> 1042

The second string has no parentheses, but it still printed 1042. Only read $Matches inside an if that checks the result of -match, as in the phone example. Microsoft documents this in about_Comparison_Operators.
Extract text without regex: IndexOf and Substring
For fixed delimiters in a predictable format, IndexOf() and Substring() work too:
$line = 'user=alice.johnson;dept=Sales;office=Austin'
$start = $line.IndexOf('dept=') + 'dept='.Length
$end = $line.IndexOf(';', $start)
$line.Substring($start, $end - $start)
Output:
Sales
It’s more code than a regex, but there’s nothing to escape. Check that IndexOf didn’t return -1 before you call Substring, or the script will stop with an error.
Pull several parts out with named groups
Name each group with ?<name> and read it from $Matches by name. It’s clearer than counting groups:
$entry = '2026-09-28 14:05:12 [WARN] Disk C: is 91% full'
if ($entry -match '^(?<date>\S+) (?<time>\S+) \[(?<level>\w+)\] (?<message>.+)$') {
"Level: $($Matches.level)"
"Message: $($Matches.message)"
}
Output:
Level: WARN
Message: Disk C: is 91% full
about_Regular_Expressions covers groups, anchors and the other regex building blocks.
Extract lines between two strings in a file
To get the lines between two marker lines, loop through the file and switch a flag on and off:
$inside = $false
foreach ($line in Get-Content -Path C:\psfaqs\deploy.log) {
if ($line -like '*END ERRORS*') { $inside = $false }
if ($inside) { $line }
if ($line -like '*BEGIN ERRORS*') { $inside = $true }
}
Output:
09:01 Web01: timeout after 30 seconds
09:02 Web03: disk space low

The order of the three checks keeps the marker lines themselves out of the result. It reads one line at a time, so it works on large logs too.
Or read the file as one string
With -Raw, the whole file is one string, and (?s) lets . match line breaks. The lazy .*? stops at the first END marker:
$log = Get-Content -Path C:\psfaqs\deploy.log -Raw
if ($log -match '(?s)BEGIN ERRORS ---\r?\n(.*?)\r?\n[^\n]*END ERRORS') {
$Matches[1]
}
Output:
09:01 Web01: timeout after 30 seconds
09:02 Web03: disk space low
Both methods returned the same two lines. Splitting strings is often simpler when the delimiters repeat in a regular pattern.
Frequently Asked Questions
How do I extract text between two characters in PowerShell?
Use a regex with a lazy capture group, like if ($text -match '\[(.*?)\]') { $Matches[1] }. Escape characters like [ and ( with a backslash.
How do I get all matches between delimiters, not just the first?
Use [regex]::Matches($text, '\[(.*?)\]') | ForEach-Object { $_.Groups[1].Value }. The -match operator only returns the first match.
Why does my regex return too much text?
.* is greedy and runs to the last delimiter. Use the lazy .*?, or a pattern like [^\]]* that can’t cross the delimiter.
Why does $Matches show a value when there was no match?
-match doesn’t clear $Matches when it fails. Read $Matches only inside an if block that checks the -match result.
How do I extract lines between two strings in a file?
Loop through Get-Content with a true/false flag that turns on at the start marker and off at the end marker, or use -Raw with a (?s) lazy regex.
For more text processing with PowerShell:
- PowerShell regex guide
- Find text with Select-String
- Split a string in PowerShell
- Replace text in strings
Bijay Kumar is an esteemed author and the mind behind PowerShellFAQs.com, where he shares his extensive knowledge and expertise in PowerShell, with a particular focus on SharePoint projects. Recognized for his contributions to the tech community, Bijay has been honored with the prestigious Microsoft MVP award. With over 15 years of experience in the software industry, he has a rich professional background, having worked with industry giants such as HP and TCS. His insights and guidance have made him a respected figure in the world of software development and administration. Read more.