Regex in PowerShell
A practical guide to pattern matching in PowerShell with -match, -replace, -split, and Select-String — and the handful of regex gotchas that catch even experienced scripters.
-match/-notmatch to test a pattern, -replace to substitute it, -split to break a string apart on it, and Select-String to search it across lines and files — and all four use the same .NET regular expression engine underneath.
Tests a string against a pattern and populates $Matches with the result.
Finds text matching a pattern and substitutes it, including captured groups.
Searches lines of text or files for a pattern, similar to grep.
What Is Regex in PowerShell?
A regular expression, or regex, is a pattern that describes text rather than spelling it out literally. Instead of checking whether a string equals "Server-01", a regex can check whether it matches the shape “Server-” followed by two digits, which also matches Server-02, Server-47, and every other server built the same way.
Think of regex as a much more precise version of the wildcards you already know from dir *.log. A wildcard like * only knows “anything, any length.” Regex can say “exactly two digits,” “one or more word characters,” or “this text, but only at the start of the line” — distinctions a wildcard simply cannot express.
PowerShell does not implement its own regex engine. Every regex-aware operator and cmdlet — -match, -replace, -split, and Select-String — hands the pattern to the same underlying .NET regular expression engine, so a pattern that works in one of them works in all of them.
c-prefixed variants — -cmatch, -creplace, -csplit — when case must matter, or -CaseSensitive on Select-String.
Why Regex Instead of Wildcards?
Wildcard matching with -like is fine for filenames and simple containment checks. Regex earns its place the moment the shape of the text matters more than a literal substring — validating an input format, pulling a value out of the middle of a line, or reshaping text on the fly.
Match “three digits, a dash, three digits, a dash, four digits” instead of a fixed string.
Pull the domain and username out of CONTOSO\jsmith in a single expression.
One pattern validates or reshapes every input that fits the same real-world format.
-like and -notlike still have their place — they are faster to read and to run for simple cases. Reach for regex once you need to describe a pattern rather than a literal, or once you need the matched text back out, not just a yes/no answer.
Why Enterprise Scripts Depend on Regex
Most of what an infrastructure script actually does is take messy, human- or log-shaped text and turn it into something structured. Regex is usually how that happens.
A script parsing the Security event log for a failed logon needs to pull the account name and domain out of a multi-line message. A script auditing usernames needs to convert DOMAIN\user into user@domain.com before it can be handed to another system. A log-tailing script needs to find every line containing ERROR or 0x8007 across a folder of rotated log files without opening each one by hand. All three are regex problems, not string-equality problems.
DOMAIN\username strings out of logon events, service account names, or CSV imports is one of the most common regex uses in an AD-facing script — the named-capture example later in this post is exactly that pattern.
Core Regex Syntax
A regex pattern is built from literal characters plus a small set of constructs that control how much and where something matches. The table below covers the constructs used in the commands further down.
| Construct | Meaning |
|---|---|
\d | Any decimal digit. \D is any non-digit. |
\w | Any word character ([a-zA-Z_0-9]). \W is any non-word character. |
\s | Any whitespace character. \S is any non-whitespace character. |
. | Any single character except a newline. |
[abc] | Any one character from the set. [^abc] matches any character not in the set. |
[a-z] | Any one character in the range. |
* + ? | Previous element zero-or-more, one-or-more, or zero-or-one times. |
{n,m} | Previous element between n and m times. {n} is exactly n, {n,} is at least n. |
^ $ | Start and end of the string (or of each line, in Multiline mode). |
(...) | A capture group. Numbered left to right; (?<name>...) gives it a name instead. |
$ means “end of string” in a regex and also triggers variable expansion inside a PowerShell double-quoted string, always wrap a regex pattern in single quotes. 'fish$' is a regex anchor; "fish$" is PowerShell trying to expand a variable called $.
Commands
The block below runs through matching, capturing, replacing, and searching in the order you would actually reach for them: test first, capture what you need, replace or split it, then scale the same pattern up to Select-String for files.
# Basic match test: does the string contain the pattern anywhere?
'PowerShell' -match '^Power\w+'
# Output: True
# -notmatch is the inverse: True when the pattern does NOT match
'bag' -notmatch 'b[iou]g'
# Output: True
# Capture groups: pull structured values out of a matched string
$string = 'The last logged on user was CONTOSO\jsmith'
$string -match 'was (?<domain>.+)\\(?<user>.+)'
# Output: True
# $Matches is a hashtable populated by the match above.
# Key 0 is always the full match; named groups become their own keys.
$Matches.domain # Output: CONTOSO
$Matches.user # Output: jsmith
# -replace uses the same capture groups in its substitution string,
# referenced with $1, $2, or ${name} for named groups
$SearchExp = '^(?<DomainName>[\w-.]+)\\(?<Username>[\w-.]+)$'
$ReplaceExp = '${Username}@${DomainName}'
'Contoso.local\John.Doe' -replace $SearchExp, $ReplaceExp
# Output: John.Doe@Contoso.local
# -replace is case-insensitive by default; -creplace forces case sensitivity
'book' -ireplace 'B', 'C' # Output: Cook
'book' -creplace 'B', 'C' # Output: book (no match, nothing replaced)
# -split breaks a string into an array on a regex delimiter
"Lastname:FirstName:Address" -split ':'
# Output: Lastname / FirstName / Address (three separate strings)
# Select-String searches lines of text or files for a pattern, like grep
Select-String -Path "C:\Logs\*.log" -Pattern 'ERROR|0x8007' -Context 2,2
# -CaseSensitive and -SimpleMatch narrow how Select-String interprets Pattern
Select-String -Path "C:\Logs\app.log" -Pattern 'Timeout' -CaseSensitive
# -Quiet returns a Boolean instead of full MatchInfo objects — useful in an if()
if (Select-String -Path "C:\Logs\app.log" -Pattern 'FATAL' -Quiet) {
Write-Warning 'Fatal error found in app.log'
}
$Matches is only overwritten on a scalar -match that returns True (or a -notmatch that returns False). A failed match, or a match against a collection instead of a single string, leaves $Matches holding whatever it held before — check the Boolean result before trusting its contents.
Quantifiers and Anchors Cheat Sheet
| Pattern | Meaning | Example |
|---|---|---|
\d{3}-\d{4} | Three digits, a dash, four digits. | Matches 555-1234 |
[A-Z]+-\d\d | One or more uppercase letters, a dash, two digits. | Matches DC-01 |
^Power\w+ | Starts with “Power”, then one or more word characters. | Matches PowerShell |
^fish$ | The entire string is exactly “fish”. | Does not match fishing |
3\.\d{2,} | Literal dot (escaped), then two or more digits. | Matches 3.141 |
Troubleshooting Cheat Sheet
| Symptom | Likely Cause | Fix |
|---|---|---|
-match works in the console but a script variable is empty |
The previous -match call returned False, or ran against a collection, so $Matches still holds an older value. |
Check the Boolean result of -match in an if before reading $Matches. |
-replace substitution outputs literal $1 or an empty value |
The replacement string was double-quoted, so PowerShell tried to expand $1 as a variable before -replace ever saw it. |
Use single quotes for the substitution string, e.g. '$1 Universe', or escape the dollar sign with a backtick in a double-quoted string. |
| Pattern meant to be case-sensitive still matches mixed case | PowerShell’s regex operators are case-insensitive by default. | Use -cmatch, -creplace, or -csplit, or add -CaseSensitive to Select-String. |
^ and $ only seem to match the whole file, not each line |
By default, ^ and $ anchor to the start and end of the entire input string, not each line. |
Pass the Multiline option to -split, or use (?m) inline in the pattern, to anchor per line instead. |
| A regex match hangs or times out on certain input | A pattern with nested quantifiers (like ^(a+)+$) can force the .NET engine into catastrophic backtracking on crafted or unusual input. |
Build a [regex] object with an explicit match timeout instead of a bare string pattern: [regex]::new($pattern, 'None', [timespan]::FromSeconds(2)). |
Final Thoughts
Regex in PowerShell is not a separate skill bolted onto the shell — it is the same .NET regex engine reached through four different doors: a Boolean test, a substitution, a splitter, and a line-oriented search tool. Once the syntax is familiar, the choice of which door to use comes down to what you need back: true/false, replaced text, an array of pieces, or matching lines from a file.
Most of the friction reported against PowerShell regex is not the regex itself — it is PowerShell’s own quoting rules colliding with regex’s use of $ and its case-insensitive defaults. Once those two habits are internalised, regex becomes one of the most reused tools in a PowerShell script.
Those same quoting rules are worth knowing well beyond regex — see PowerShell String Formatting — Here-Strings, -f, and Interpolation Gotchas for the full set of interpolation and here-string pitfalls.
$Matches, and reach for -cmatch/-creplace/-csplit the moment case actually matters.
Next, we can cover PowerShell string formatting: here-strings, the -f operator, and the interpolation gotchas that trip up scripts moving between single- and double-quoted strings.