Filtering and extracting text with regular expressions: basic patterns with metacharacters and quantifiers, the -match and -replace operators, searching files with Select-String, static methods of the regex class, common patterns for email, IP, date, and phone number, plus best practices for fast and readable regex.

In episode 10 you worked with structured data: CSV, JSON, and XML — all neatly organized. But the real world is messier: server logs, output from legacy programs, and user input are free text without structure. To filter, validate, and extract from such text, you need regular expressions — patterns that recognize text.
Think of regex as a metal detector: it doesn't look for exact text, but for patterns — "find everything that looks like an IP address", "find all invoice numbers". You touched the concept via the -match operator in episode 4; episode 11 covers it thoroughly.
Regex is built from literals (exact text) and metacharacters (symbols with special meanings):
| Symbol | Meaning | Example |
|---|---|---|
. | Any single character | a.c matches abc, axc |
* | Zero or more of the previous | ab*c matches ac, abc, abbc |
+ | One or more of the previous | ab+c matches abc, abbc |
? | Zero or one of the previous | colou?r matches color, colour |
^ | Start of string | ^ERROR matches strings starting with ERROR |
$ | End of string | done$ matches strings ending with done |
In PowerShell, the -match and -replace operators use regex. Note the importance of the ^ and $ anchors — without them, -match "ERROR" also matches "NOT_AN_ERROR" because the pattern is found in the middle:
"NOT_AN_ERROR" -match "ERROR"
"NOT_AN_ERROR" -match "^ERROR"
"abc" -match "a.c"
"axc" -match "a.c"The character class [...] matches one character from the set:
"a1" -match "^[a-z][0-9]$"
"Z9" -match "^[A-Z][0-9]$"
"abc" -match "^[a-c]+$"[a-z] is the lowercase range, [0-9] the number range, [A-Z] uppercase. Combinations like [a-zA-Z0-9] cover letters and numbers. There are also common shorthand classes: \d digits, \w letters-or-numbers-or-underscore, \s whitespace.
"Tahun 2026" -match "\d{4}"Quantifiers specify the number of repetitions:
| Quantifier | Meaning |
|---|---|
* | 0 or more |
+ | 1 or more |
? | 0 or 1 |
{n} | exactly n times |
{n,} | n times or more |
{n,m} | between n and m times |
"1234" -match "^\d{4}$"
"2026-08-03" -match "^\d{2,4}-\d{1,2}-\d{1,2}$"
"a" -match "^a{1,3}$"
"aaa" -match "^a{1,3}$"{n,m} limits the range — the date 08-03 matches \d{1,2} because one or two digits are both accepted.
Three main operators for using regex:
-match — returns true or false and populates $matches.-notmatch — its opposite.-replace — replaces matching parts with new text."web-01" -replace "^web-", "app-"
"invoice-2026.pdf" -replace "\d{4}", "2027"
"2026-08-03" -replace "-", "/"-replace can use patterns and replacement strings that include capture groups — for example swapping date formats:
"2026-08-03" -replace "^(\d{4})-(\d{2})-(\d{2})$", "$3/$2/$1"When -match succeeds, the matching result is stored in the automatic variable $matches — a dictionary: the whole match in key 0, each capture group (parts inside parentheses) in keys 1, 2, and so on:
if ("server-42" -match "^(server)-(\d+)$") {
$matches[1]
$matches[2]
}Capture groups make extraction precise: not just "does it match?", but "which part matches?" — the $matches[1] and $matches[2] pair is data you can use directly.
Naming groups makes code more readable than remembering numbers:
if ("web-01:8080" -match "^(?<host>[^:]+):(?<port>\d+)$") {
$matches.host
$matches.port
}With the (?<name>...) syntax, dictionary keys are named — $matches.host and $matches.port read like object properties.
Select-String searches for text inside files — like grep in the terminal:
Select-String -Path /var/log/syslog -Pattern "ERROR"
Get-ChildItem /var/log -File | Select-String -Pattern "ERROR"Search configurations you'll use often:
-Context — shows lines before and after a match.-CaseSensitive — case-sensitive matching (default: not case-sensitive).Select-String -Path /var/log/syslog -Pattern "ERROR", "WARN" -Context 1, 2Select-String output is MatchInfo objects with Line, LineNumber, Filename, and Pattern properties — data ready for pipeline processing:
Select-String -Path /var/log/syslog -Pattern "ERROR" |
Select-Object Filename, LineNumber, LineFor full control, use the .NET [regex] class with its static methods:
[regex]::Match("2026-08-03", "\d+").Value
[regex]::Matches("a1 b22 c333", "\d+") | ForEach-Object { $_.Value }
[regex]::Replace("2026-08-03", "-", "/")
[regex]::Split("a,b,c", ",")| Method | Function |
|---|---|
Match | First match, a Match object |
Matches | All matches, a loopable collection |
Replace | Replace all matches |
Split | Split a string based on a pattern |
[regex]::Matches returns a collection — each element has Value, Index, and Groups. Useful when you need all matches at once, not just the first.
Patterns you'll almost certainly need in real scripts:
$email = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
$ipv4 = "^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$"
$tanggal = "^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$"
$hp = "^\+?62[0-9]{8,13}$"
"user@contoh.com" -match $email
"192.168.1.10" -match $ipv4
"2026-08-03" -match $tanggal
"+6281234567890" -match $hpThe IP pattern above looks complicated, but its logic is organized: each octet must be 0-255, expressed via alternatives 25[0-5] (250-255) or 2[0-4]\d (200-249) or others. Store patterns like these as named constants at the top of your scripts — with names, the intent is clear without parsing character by character.
Warning
Validating patterns in production needs extra care: the email/IP patterns above are sufficient for most cases, but not perfect for every situation. Real examples: -match is case-insensitive by default and doesn't require a full match without the ^ and $ anchors. Always test against a real data set before relying on it to block input.
Tips that save you from regex nightmares:
^ and $ when you want a full match; without them, -match matches wherever the pattern appears.\. matches a dot, \* matches a star.* and + quantifiers are greedy (taking as much as possible). Use *? or constrain with anchors when needed.(?<name>...) is clearer than remembering group numbers.-like or .Contains is much faster.$pola = "^[a-z0-9_-]+$"
@("nama_valid", "NAMA SALAH!", "user-1") | ForEach-Object {
"$_ -> $($_ -match $pola)"
}Episode 11 completes your text-filtering arsenal: the basic metacharacters . * + ? ^ $; character classes and shorthands like \d; quantifiers with ranges; the -match, -notmatch, and -replace operators; $matches along with capture groups and named groups; file searching with Select-String; static methods of the [regex] class; common patterns for email, IP, date, and phone numbers; and best practices for regex that's fast and painless.
Key takeaways:
-match doesn't require a full match — use ^ and $.$matches[1] takes a capture group; (?<name>...) names it.Select-String is PowerShell's grep with object output.[regex]::Matches gives all matches, not just the first.The regular expressions material closes the foundation section: conditionals, loops, functions, pipeline, files, structured data, and text patterns — you can now read and write real scripts. But real scripts will inevitably face failures. In episode 12 we learn error handling — turning failure from a mysterious enemy into a controlled event. See you in episode 12!