1 The Problem
We want to build a regex engine: match patterns with . (any character) and * (zero or more of the previous) against text. We will implement it with recursion that mirrors the formal definition of matching. It teaches the theory behind regular expressions — recursion, backtracking, and how an elegant definition becomes working code.
2 How to Think About It
Think recursively about matching, before any code:
* means “zero or more”: try matching zero (skip it) OR one-then-recurse. → 4. A normal character (or .) must match here, then recurse on the rest. The recursion mirrors the definition exactly.
3 The Build — explained part by part
Here is a complete regex engine. It is short but deep — read each part’s explanation carefully.
def match(pattern, text):
# Does `pattern` match starting at the beginning of `text`?
if not pattern:
return True # empty pattern matches (we are done)
# Is the next token followed by a star? (e.g. "a*")
if len(pattern) >= 2 and pattern[1] == "*":
return match_star(pattern[0], pattern[2:], text)
# A normal character or "." must match the first text character.
first_matches = bool(text) and pattern[0] in (text[0], ".")
if first_matches:
return match(pattern[1:], text[1:])
return False
def match_star(char, rest_pattern, text):
# "char*" means zero or more of char.
# Option 1: match zero — skip the star, try the rest now.
if match(rest_pattern, text):
return True
# Option 2: match one char here, then try star again (backtrack).
while text and (char == text[0] or char == "."):
text = text[1:]
if match(rest_pattern, text):
return True
return False
if __name__ == "__main__":
print(match("a*b", "aaab")) # True
print(match("a.c", "axc")) # True
print(match("a*b", "aaac")) # False
pattern[1] == "*" — look one ahead: if the next character is a star, this is a “zero or more” situation, handled specially by
match_star. We pass the character, the pattern after the star, and the text.pattern[0] in (text[0], ".") — a character matches if it equals the text’s character, or if the pattern character is
. (the wildcard). Then we recurse on the rest of both.match_star — option 1: try matching zero of the character by skipping straight to the rest of the pattern. If that succeeds, done.
match_star — option 2: otherwise consume one matching character and try again. This loop is backtracking: try consuming more and more, checking if the rest matches each time. If nothing works, return False.
why recursion? — matching is naturally recursive: “the pattern matches if the first part matches and the rest matches”. The code mirrors that definition almost word for word.
* — then a*b cannot match "b".text is non-empty before text[0] — crashes at the end.bool(text) before indexing.pattern[1] without checking length — IndexError on a one-char pattern.len(pattern) >= 2 before peeking ahead.4 Test & Prove Each Part
We test literal matches, the wildcard, and the star — including tricky cases where backtracking matters.
def match(pattern, text):
if not pattern:
return True
if len(pattern) >= 2 and pattern[1] == "*":
return match_star(pattern[0], pattern[2:], text)
first_matches = bool(text) and pattern[0] in (text[0], ".")
if first_matches:
return match(pattern[1:], text[1:])
return False
def match_star(char, rest_pattern, text):
if match(rest_pattern, text):
return True
while text and (char == text[0] or char == "."):
text = text[1:]
if match(rest_pattern, text):
return True
return False
def test_literal():
"""A literal pattern matches identical text."""
assert match("abc", "abc") is True
def test_wildcard():
""". matches any single character."""
assert match("a.c", "axc") is True
def test_star_many():
"""a* matches several a's."""
assert match("a*b", "aaab") is True
def test_star_zero():
"""a* matches zero a's."""
assert match("a*b", "b") is True
def test_no_match():
"""A non-matching pattern returns False."""
assert match("a*b", "aaac") is False
Run with pytest -v. The test_star_zero case — a*b matching just "b" — proves the “match zero” branch works. The two star cases together show why both options in match_star are needed.
5 The Interface
Examples
match("a*b", "aaab") -> True
match("a.c", "axc") -> True
match("a*b", "aaac") -> False6 Run It & Automate It
python3 regex.pyTry patterns with
. and *. This is a real (if minimal) regex engine.Jenkins runs the tests automatically — each line explained below.
True
True
Falsea*b fails to match "b".match(rest, text) first in match_star.IndexError.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Anchors. Support
^(start) and$(end). (Teaches: position matching.) - Plus. Add
+(one or more). (Teaches: a variation on star.) - Character classes. Support
[abc]. (Teaches: sets in patterns.)