Regex Workbench
Regular expressions fail in a characteristic way: they work on the three examples you tried and silently mangle the fourth you didn't. This skill treats a regex as a small program with a test suite — built incrementally against a corpus of real positive and negative examples, checked for pathological performance, and documented so the next reader can change it without fear.
When to use this skill
- Writing any regex that will run against data you did not personally type
- Debugging a pattern that matches too much, too little, or hangs on certain inputs
- Reviewing a regex in a diff and deciding whether it is safe to approve
- Extracting structure from logs, filenames, user input, or scraped text
- Deciding whether a regex is the right tool at all — often it is not (see Guardrails)
Workflow
- Build the corpus before the pattern. Collect 5-10 strings that must match and, just as important, 5-10 near-misses that must not. Pull them from real data. If you cannot produce a near-miss, you do not understand the format yet.
- Write the spec in words. One sentence: "an order ID is
ORD-, then 4-10 digits, at a word boundary." If the sentence needs three clauses of exceptions, consider a real parser instead. - Assemble incrementally. Start from the literal skeleton, generalize one piece at a time, and re-run the corpus after every change. Never write the full pattern in one keystroke.
- Anchor deliberately. Decide explicitly: whole-string (
^...$), word-boundary (\b), or free-floating. Unanchored patterns are the leading source of matches-too-much bugs. - Prefer explicit character classes over
., and bounded quantifiers ({1,64}) over unbounded+and*wherever the data has known limits. - Name what you capture. Use named groups where the flavor supports them, and non-capturing
groups
(?:...)for pure grouping, so group numbers never silently shift. - Probe the pathological cases: empty string, the delimiter itself, doubled delimiters, unicode lookalikes, the longest plausible input. Every interesting probe joins the corpus permanently.
- Check for catastrophic backtracking. Nested quantifiers over overlapping classes —
(a+)+,(\w+\s*)+— are the signature. Test against a 10,000-character non-matching string; if matching stalls, restructure the pattern or use atomic/possessive constructs where available. - Document at the call site: the one-sentence spec, the engine and flags assumed, and one example match.
Output format
Deliver the pattern with its evidence, never alone:
Pattern:
\bORD-(?P<digits>\d{4,10})\b
Spec: order IDs like ORD-12345 at word boundaries; 4-10 digits; case-sensitive.
Flavor: <engine and flags assumed>
Must match: ORD-1234 | "ref: ORD-9876543," | ORD-0000000001
Must NOT match: ORD-123 | XORD-1234 | ORD-12345678901 | ord-1234
Guardrails
- Do not parse nested or recursive structures — markup, balanced parentheses, serialized objects — with a single regex; reach for a real parser
- Validate untrusted input length before matching; regexes are a denial-of-service surface
- A regex over ~80 characters without comments or verbose mode is a maintenance incident on layaway
- When doing replacements, run the negative corpus too — a pattern that "only matches the right things" can still rewrite the wrong text via backreferences
- Escape user-supplied fragments with the language's escape helper before interpolating them into a pattern
Debugging checklist
- Reproduced the failure with a minimal input, now saved in the corpus
- Checked anchoring — is the match allowed to start where you assume it starts?
- Checked greediness — would a lazy quantifier or a tighter class fix it?
- Checked flags — case sensitivity, multiline
^$semantics, whether dot crosses newlines - Checked the flavor — the same pattern means different things in different engines
- Added the fixed case to the must-match or must-not list so it stays fixed