How to Extract Text Between Delimiters in PowerShell

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
PowerShell extract text between brackets with greedy vs lazy regex
Greedy .* grabs too much, lazy .*? stops at the first bracket (PowerShell 7)

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
PowerShell $Matches keeps the previous value when -match fails
The second line shows the first line’s result (Windows PowerShell 5.1)

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
PowerShell extract lines between two marker strings in a log file
The lines between the BEGIN and END markers (PowerShell 7)

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: