Regex looks intimidating until you’ve internalized maybe a dozen symbols — after that it’s just the fastest way to validate, search, or extract text by pattern instead of writing a string-parsing function by hand. This is a working reference: the symbols that come up constantly, three real examples in different languages, and when it’s actually the wrong tool.
In this post: Basic syntax · Sample implementations · Capture groups and lookaheads · Tradeoffs · Catastrophic backtracking is a real DoS vector · When to reach for something else · When would you actually use this? · Related reading
Basic syntax
| Symbol | Meaning |
|---|---|
. | Matches any single character |
\d | Matches any digit (0-9) |
\D | Matches any non-digit character |
\w | Matches any alphanumeric character (a-z, A-Z, 0-9, _) |
\s | Matches any whitespace character |
* | Matches zero or more occurrences |
+ | Matches one or more occurrences |
? | Matches zero or one occurrence |
^ | Anchors to the start of the string |
$ | Anchors to the end of the string |
| | Acts as an OR operator |
[] | Matches any single character inside the brackets |
{n,m} | Matches between n and m occurrences |
\b | Matches a word boundary (e.g. \bword\b matches “word” but not “sword” or “wording”) |
Sample implementations
Matching an email address (JavaScript):
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailRegex.test("test@example.com")); // true
console.log(emailRegex.test("invalid-email")); // false
Validating a number (Python):
import re
number_regex = re.compile(r"^-?\d+(\.\d+)?$")
print(bool(number_regex.match("123"))) # True
print(bool(number_regex.match("-123.45"))) # True
print(bool(number_regex.match("abc"))) # False
Extracting dates from text (PowerShell):
$text = "Today's date is 2026-02-13 and yesterday was 2026-02-12."
$dates = [regex]::Matches($text, '\b\d{4}-\d{2}-\d{2}\b')
$dates.Value # PowerShell's member enumeration returns each match's .Value: '2026-02-13', '2026-02-12'
Capture groups and lookaheads
Worth knowing these beyond the basic symbol table above, since they’re what separates “does this string match” from “pull the specific piece I actually need out of it”:
(...)— a capture group; the matched substring is retrievable afterward instead of just knowing the overall pattern matched.(?<name>...)— a named capture group, retrievable by name (match.groups.namein JavaScript) instead of a numbered index that breaks if the pattern’s group order ever changes.(?=...)— a positive lookahead: matches a position only if what follows matches the pattern, without consuming those characters as part of the match itself.(?!...)— a negative lookahead: matches a position only if what follows doesn’t match the pattern.
A concrete case where lookaheads earn their complexity — a password rule requiring at least one digit and one uppercase letter, in any order, without capturing or consuming either:
const passwordRegex = /^(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(passwordRegex.test("Password1")); // true
console.log(passwordRegex.test("password")); // false -- no digit, no uppercase
Trying to express “contains a digit AND an uppercase letter, in either order” without lookaheads means enumerating both orderings explicitly — lookaheads let each condition stand independently instead.
Tradeoffs
Regex is concise, works across nearly every language and text tool, and handles large volumes of text fast once a pattern is written correctly. The costs are real too: complex patterns get genuinely hard to read and debug, a poorly written pattern can be slow (or in pathological cases, catastrophically slow — “catastrophic backtracking” is a real performance failure mode, not a theoretical one), and engines differ slightly between languages, so a pattern that works in JavaScript isn’t guaranteed to behave identically in Python or .NET. A regex that made sense when you wrote it can be nearly unreadable to whoever maintains it a year later — comment non-obvious patterns.
Catastrophic backtracking is a real DoS vector
The tradeoffs section above flags catastrophic backtracking as a real performance failure mode — worth being specific about what actually triggers it, since it’s the kind of bug that passes every normal test case and only shows up against a crafted input. Nested or overlapping quantifiers are the usual cause: a pattern like ^(a+)+$ looks harmless, but against a long string of “a”s followed by one non-matching character, the engine tries an exponential number of ways to split those repeated groups before giving up. This is a real, named vulnerability class — ReDoS (Regular Expression Denial of Service) — not a theoretical edge case: a single crafted input to a regex like that can hang a Node.js event loop for seconds or minutes, blocking every other request on that process while it runs.
Any regex validating user-supplied input (a form field, a URL parameter, an uploaded file name) is a potential target if it contains nested quantifiers. Three practical mitigations, since JavaScript’s regex engine doesn’t support atomic groups (the cleanest fix, available in .NET and some other engines) at all:
- Rewrite nested quantifiers so an inner group can’t be matched by the outer one in more than one way — often the actual fix rather than a workaround.
- Cap input length before it reaches the regex — a hard length check ahead of time bounds how bad the worst case can get, even against a pattern you haven’t fully audited.
- Run untrusted-input matching in a worker thread with a timeout —
RegExp.prototype.test()has no built-in timeout in JavaScript, so this has to be enforced externally if a pattern absolutely can’t be rewritten safely.
When to reach for something else
| Feature | Regex | String methods (.contains(), .replace()) |
|---|---|---|
| Complexity handling | High | Low |
| Ease of use | Steeper learning curve | Easy |
| Pattern precision | Extremely precise | Limited to exact/substring matches |
- Simple substring checks —
indexOf(),.contains(), or.split()are more readable than a regex for something that basic. - Structured data like JSON or XML — use the actual parser (
JSON.parse(), an XML library) rather than a regex trying to approximate one; regex-based HTML/XML parsing breaks on edge cases a real parser handles correctly. - Genuinely fuzzy natural-language matching — regex is precise pattern matching, not language understanding; that’s a different tool entirely.
A regex pattern that made sense the day you wrote it can be nearly unreadable a year later — comment anything non-obvious while the logic is still fresh.
When would you actually use this?
- Validating user input against a specific format — email, phone number, a required naming convention — before it hits your data layer.
- Parsing log files for structured pieces (timestamps, IP addresses, error codes) that a simple string search can’t isolate cleanly.
- Search-and-replace across a large body of text where the pattern being replaced isn’t a fixed string, just a shape (any date, any number, any capitalized word).
Related reading
- How to Escape Apostrophes in SharePoint REST Queries — a narrower, real-world string-pattern problem regex-adjacent logic runs into constantly.
- Store and Display Dates Properly in JavaScript — pairs with the date-extraction example above once you have the matched string and need to work with it as an actual date.
Hope that saves you some time — drop a comment if anything’s unclear.
App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning


