how-to · 6 min read
Regular expressions: a practical starting guide
A regular expression is a pattern that describes a set of strings. It can check whether text matches, find every occurrence, capture parts of a match, or find and replace with references to those captures. Most programming languages and many command-line tools implement the same core syntax with small differences.
The difficulty is not writing a pattern once; it is understanding why a pattern matches more or less than intended. This guide covers the building blocks, the matching model that explains surprising results, and the safety limits worth knowing before you run a pattern on untrusted input.
A regular expression is a description, not a search string
A literal search finds the exact characters you typed. A regular expression interprets some characters as instructions, so the pattern abc matches the characters abc, while a.c matches any three-character string that starts with a and ends with c. That interpretation is what makes regex powerful and what makes an unescaped dot a common bug.
Regular expressions are usually applied to a line or a whole string depending on flags, and that context changes the result. The same pattern might match one substring on a single line and something entirely different once newlines are included. Always confirm whether the engine treats the input as one string or as separate lines before debugging the pattern itself.
The building blocks worth learning first
Character classes match one character from a set: [abc] matches a, b or c; [a-z] matches any lowercase ASCII letter; d is a digit; w is a word character; s is whitespace. Negated classes flip the meaning, so [^0-9] matches anything that is not a digit. Anchors do not consume characters: ^ matches the start of the string or line and $ matches the end.
Quantifiers say how many times the previous element repeats: * means zero or more, + means one or more, ? means zero or one, and {2,5} means between two and five. By default they are greedy and take as much as possible, then give characters back only if the rest of the pattern needs them; adding ? makes them lazy and takes as little as possible. Groups in parentheses capture what matched, and a pipe creates alternatives such as cat|dog.
- d+ matches one or more digits; ^d{5}$ matches exactly five digits on its own.
- word matches the whole word, not the word inside another word.
- ([A-Za-z]+)s+([A-Za-z]+) captures two words separated by whitespace.
- a.*?b matches from the first a to the nearest b, while a.*b reaches the last b.
- Flags change behaviour: i ignores case, g finds all matches, m treats ^ and $ as line anchors, s lets dot match newlines, u enables full Unicode semantics.
How matching actually proceeds
Engines try the pattern at successive positions in the input (unless it is anchored), and at each position they explore alternatives. When a greedy quantifier has consumed characters and the rest of the pattern fails, the engine backtracks and tries a shorter repetition. That backtracking is why a pattern can match correctly and still be slow: the number of paths it explores can grow exponentially with the input.
Global matching is a loop around that process. A pattern with the g flag returns successive matches, and zero-length matches advance the position specially to avoid an infinite loop. Capture groups are numbered by the order their opening parenthesis appears, and named groups make complex patterns far easier to read and maintain.
A debugging workflow that works
Start from the smallest input that should match and the nearest input that should not. Test both, then add complexity one element at a time instead of writing the full pattern and staring at the result. When a match is wrong, ask three questions: where did the match start, where did it end, and which alternative or repetition consumed the characters in between.
Escape characters that the engine treats specially. This is the most common source of confusion when testing URLs, file paths, email addresses and code snippets. Remember that JSON and programming-language string literals also process backslashes, so a pattern written for a regex engine may need the backslash doubled in source code. Use a tester that shows both the pattern you typed and the pattern the engine received.
Safety: catastrophic backtracking and untrusted input
Some patterns are correct but catastrophically slow on particular inputs. Nested quantifiers such as (a+)+$ are the classic example: for a long run of characters followed by one that cannot match the end anchor, the engine tries an exponential number of ways to divide the run. The practical defences are to simplify nested repetition, anchor patterns where possible, prefer explicit character classes, and enforce a timeout or input-length limit when matching untrusted text.
Never use a regular expression as the only defence for security-sensitive parsing. Email addresses, HTML, URLs and programming languages all exceed what a compact pattern can validate correctly. Use a regex for a quick sanity check, then parse with a dedicated library or validate structurally. And when a pattern runs on a server, cap the input size and the number of matches so a single request cannot consume the process.