Regex exercises
Practice tasks for Regular expressions - Python re + bash grep/sed. Each has a Python solution and, where useful, a bash equivalent.
Character classes & anchors
Filter RPMs by prefix
For a literal prefix, str.startswith() is cleaner; use regex when the prefix
itself is a pattern.
import re
def find_rpms(rpms, prefix):
pat = re.compile(r'^' + re.escape(prefix))
return [r for r in rpms if pat.match(r)]
Extract individual digits
\d (single) vs \d+ (groups of digits):
def extract_digits(s):
return [int(d) for d in re.findall(r'\d', s)] # "4.19" -> [4,1,9]
Quantifiers
Extract version numbers (X.Y or X.Y.Z)
Use a non-capturing group so findall returns the full match, not the
optional .Z group:
def extract_versions(text):
return re.findall(r'\d+\.\d+(?:\.\d+)?', text)
Extract quoted strings (greedy vs lazy)
[^"]* (any char except quote) is correct and faster than lazy .*?:
def extract_quoted(s):
return re.findall(r'"([^"]*)"', s)
Groups and capturing
Parse an RPM NVR
Greedy (.+) for the name backtracks to the last hyphen before a digit, so it
handles hyphenated names; \d[\d.]*\d keeps the version digit-bounded.
def parse_nvr(nvr):
m = re.match(r'(?P<name>.+)-(?P<version>\d[\d.]*\d)-(?P<release>\d+\.\w+)$', nvr)
if not m:
raise ValueError(f"Invalid NVR: {nvr}")
return m.groupdict()
Parse os-release (quoted and unquoted)
("?) captures an optional opening quote; \2 backreferences the same
character at the end:
def parse_os_release(text):
pairs = re.findall(r'^(\w+)=("?)(.+?)\2$', text, re.M)
return {k: v for k, _, v in pairs}
Alternation and anchors
Filter by OS variant
def filter_images(images):
return [i for i in images if re.search(r'rhel|fedora', i)]
Whole-word match
\bcore\b matches core and core-utils but not coreos/coreutils
(- is a word boundary, o is not). In bash, grep -w does the same.
def find_core(packages):
return [p for p in packages if re.search(r'\bcore\b', p)]
Substitution
Mask IP addresses
def mask_ips(log):
return re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '[REDACTED]', log)
Strip the minor version from repo names
Capture the part to keep, match-but-don’t-capture the rest:
def rewrite_repos(repos):
return [re.sub(r'(rhel-\d+)\.\d+', r'\1', r) for r in repos]
# "rhel-9.8-baseos" -> "rhel-9-baseos"; "epel-9" unchanged
Lookahead and lookbehind
Extract a value after a label
Lookbehind asserts KEY= precedes the match without including it; \b keeps
ARCH from matching inside BASEARCH. The capture-group version is more
common in practice:
def get_value(meta, key):
m = re.search(rf'\b{re.escape(key)}=(\S+)', meta)
return m.group(1) if m else None
Match a version only before a suffix
def el9_versions(artifacts):
text = '\n'.join(artifacts)
return re.findall(r'\d+\.\d+\.\d+(?=-\d+\.el9)', text)
Multiline and flags
Parse ARG definitions from a Containerfile
re.M makes ^ match each line; the ("?)...\2 trick handles optional
quotes (empty value matches too):
def extract_args(containerfile):
pairs = re.findall(r'^ARG\s+(\w+)=("?)([^"\n]*)\2', containerfile, re.M)
return {k: v for k, _, v in pairs}
Real-world combined
Parse kola test results
finditer yields Match objects, so named groups are available; convert types
as you build the dict:
def parse_kola_results(output):
pat = re.compile(r'--- (?P<status>PASS|FAIL|SKIP): (?P<name>\S+) \((?P<duration>[\d.]+)s\)')
return [{"name": m["name"], "status": m["status"],
"duration": float(m["duration"])} for m in pat.finditer(output)]
Validate and normalize image references
fullmatch rejects malformed strings; the optional tag group lets you add a
default:
IMAGE_RE = re.compile(
r'(?P<registry>[\w.-]+(?::\d+)?)' # registry (optional port)
r'/(?P<path>[\w./-]+)' # image path
r'(?::(?P<tag>[\w.+-]+))?' # optional tag
)
def normalize_images(images):
out = []
for img in images:
m = IMAGE_RE.fullmatch(img)
if m:
out.append(img if m["tag"] else f"{img}:latest")
return out
Extract failed tests from a CI log
Match the error line followed by the FAIL line (relies on consistent Go test output):
def extract_failures(ci_log):
return [{"test": m.group(2), "error": m.group(1)}
for m in re.finditer(r'Error: (.+)\n--- FAIL: (\S+)', ci_log)]