Regular Expressions & Advanced String/Text Processing

PowerShell Intermediate/Advanced

Chapter 2 · Regular Expressions & Advanced String/Text Processing

PowerShell Fundamentals 4 introduced -match as one comparison operator among several — enough to filter a pipeline, not enough to actually use regular expressions well. This chapter goes back to -match and gives it real depth: the automatic variable it quietly populates, capturing named pieces out of a match, regex-powered find-and-replace, and — the chapter's own central gotcha — exactly why -match alone can't find every match in a string, no matter how many times that string actually contains one.

-match & $Matches: A Comparison That Remembers What It Found

Unlike every other comparison operator from Fundamentals 4, a successful -match does something extra: it populates the $Matches automatic variable with the details of what actually matched.

"The year is 2024" -match '\d{4}' # True $Matches[0] # "2024" — the whole match

Capture Groups: Numbered and Named

# Numbered groups — accessed by position, 1-based ($Matches[0] is always the whole match) "2024-08-05" -match '(\d{4})-(\d{2})-(\d{2})' $Matches[1] # 2024 $Matches[2] # 08 # Named groups — (?<name>...) — accessed by name, often much more readable "John Smith" -match '(?<first>\w+)\s(?<last>\w+)' $Matches['first'] # John $Matches['last'] # Smith

-replace: Regex-Based Replacement With Backreferences

"2024-08-05" -replace '(\d{4})-(\d{2})-(\d{2})', '$2/$3/$1' # 08/05/2024 — $1/$2/$3 refer back to the numbered capture groups above

-replace's replacement string reads the same capture groups -match would have populated into $Matches — just referenced as $1, $2, $3 (or ${name} for a named group) directly inside the replacement text itself, rather than through a separate variable.

Case Sensitivity: Insensitive by Default

Consistent with every comparison operator back in Fundamentals 4, -match and -replace are case-insensitive by default — a genuinely different default from most languages' own regex engines. A -c-prefixed variant exists for the rare case a real case-sensitive match is actually needed:

"HELLO" -match "hello" # True — case-insensitive by default "HELLO" -cmatch "hello" # False — the case-sensitive variant "HELLO" -creplace "hello", "hi" # no match, nothing replaced

When -match Isn't Enough: [regex]::Matches()

$text = "Contact: alice@example.com or bob@example.org" $text -match '\w+@\w+\.\w+' $Matches[0] # alice@example.com — only the FIRST match, the second one is nowhere to be found # [regex]::Matches finds EVERY match in the string, not just one [regex]::Matches($text, '\w+@\w+\.\w+') | ForEach-Object { $_.Value } # alice@example.com # bob@example.org
The central fact this chapter is built on
-match against a single string only ever tells you two things: whether a match exists at all, and — if so — the details of exactly one match, in $Matches. It was never designed to enumerate every occurrence inside a longer string, no matter how many times that pattern genuinely appears. [regex]::Matches() — the real .NET method underneath, reached via the [regex] type accelerator — returns a full collection of every match found, each with its own .Value and its own .Groups. Reach for -match when you're testing a whole string against a pattern; reach for [regex]::Matches() the moment "how many times does this appear" or "find every instance" is the actual question.
$Matches gets silently overwritten by the next -match
$Matches isn't scoped to one expression — it's a shared automatic variable that any later -match call quietly replaces:
"abc" -match 'a' $first = $Matches[0] # captured immediately — safe "xyz" -match 'x' # $Matches now holds the xyz result — $first is still fine only because it was saved separately first
Reading $Matches any time after a second -match has run, expecting the first result to still be sitting there, is a real, easy mistake — always assign what you need out of $Matches immediately after the -match that produced it.

-split With a Real Regex Pattern

Fundamentals 5 used -split with a plain literal separator. -split's right-hand side is always a regex, which becomes genuinely useful once the separator itself is a pattern rather than a fixed string:

"one1two22three333four" -split '\d+' # one, two, three, four — split on any run of one or more digits, whatever their length
A first practical habit
Before reaching for [regex]::Matches() or a more complex pattern, try the simplified -match/$Matches form first on a single sample string — it's faster to write and read for the common "does this match, and what did it capture" case, and it's the same underlying regex engine either way.

Hands-On Exercises

Exercise 1

Explain why "HELLO" -match "hello" returns $true, tying it back to Fundamentals 4's own case-insensitivity of -eq. What operator would you use instead for a genuinely case-sensitive match?

📄 View solution
Exercise 2

Given a string containing three email addresses, explain why -match and $Matches only ever surface one of them. What's the correct tool for finding all three, and how would you use it?

📄 View solution
Exercise 3

Write a single -replace expression that reformats a date string from "2024-08-05" (YYYY-MM-DD) to "08/05/2024" (MM/DD/YYYY), using regex capture groups and backreferences.

📄 View solution

Chapter 2 Quick Reference

  • -match — regex comparison; populates $Matches on success
  • $Matches[0] / $Matches[N] / $Matches['name'] — whole match / numbered group / named group
  • -replace — regex-based replacement; $1/$2/${name} in the replacement string reference capture groups
  • Case-insensitive by default-cmatch/-creplace/-csplit for case-sensitive variants
  • [regex]::Matches($text, $pattern) — returns every match in a string; -match alone only ever reports one
  • $Matches is shared and gets overwritten — assign what you need out of it immediately, before the next -match runs
  • -split — its right-hand side is always a regex, not just a literal separator
  • Next chapter: PowerShell Remoting (WinRM, Invoke-Command, PSSessions)