Find & Replace — Python source
Find and replace text with literal or regular-expression matching, global replace, case sensitivity, whole-word, and capture-group substitution. Live match counter.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""Find & replace with literal or regex matching, $-substitution
($1 backrefs, $&, $$), case sensitivity, whole-word, and global modes.
Language: Python
CosmoDev polyglot showcase port of the ``find-replace`` tool.
Ported from src/lib/findReplace.ts — display source, part of CosmoDev's
polyglot tool pages.
Mirrors the live lib: a literal find string is regex-escaped (re.escape) and
matched verbatim; an isRegex find is compiled as-is. ``\\b`` wraps the
pattern when wholeWord is set, and re.compile composes re.IGNORECASE and
re.MULTILINE (the JS i / m flags). Invalid patterns raise re.error, caught
and returned as an error string (the lib never throws), and an empty find
is a no-op.
Replacement $-substitution is implemented in expand_replacement (not via
re.sub's native ``\\1`` syntax) so it matches JavaScript's String.replace
exactly for the realistic cases: ``$$`` -> ``$``, ``$&`` -> whole match,
``$1``..``$99`` -> capture group (literal ``$<digits>`` when out of range).
JS's ``$``` and ``$'`` (text before/after the match) are unsupported.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Optional
@dataclass
class Options:
"""Mirror of the TypeScript lib's FindReplaceOptions.
``global_`` is suffixed because ``global`` is a Python keyword.
"""
is_regex: bool = False
case_sensitive: bool = True
whole_word: bool = False
global_: bool = True
multiline: bool = False
@dataclass
class FindReplaceResult:
result: str = ""
matches: int = 0
error: Optional[str] = None
def _build_regex(find: str, o: Options):
"""Compile find with flag + whole-word modifiers.
Returns a compiled pattern, or the engine's error string on invalid
syntax (the lib's buildRegex return).
"""
pattern = find if o.is_regex else re.escape(find)
if o.whole_word:
pattern = rf"\b{pattern}\b"
flags = 0
if not o.case_sensitive:
flags |= re.IGNORECASE
if o.is_regex and o.multiline:
flags |= re.MULTILINE
try:
return re.compile(pattern, flags)
except re.error as e:
return str(e)
def _expand(template: str, whole: str, groups: list, num_groups: int) -> str:
"""Apply JS String.replace $-substitution for one match.
``$$`` -> ``$``; ``$&`` -> whole match; ``$1``..``$99`` -> capture
group N (literal ``$<digits>`` when N is out of range, matching JS).
"""
out: list = []
i, n = 0, len(template)
while i < n:
c = template[i]
if c != "$":
out.append(c)
i += 1
continue
nxt = template[i + 1] if i + 1 < n else ""
if nxt == "$":
out.append("$")
i += 2
elif nxt == "&":
out.append(whole)
i += 2
elif nxt.isdigit():
d1 = int(nxt)
# Greedily try a second digit ($nn), matching JS.
if i + 2 < n and template[i + 2].isdigit():
d2 = d1 * 10 + int(template[i + 2])
if 1 <= d2 <= num_groups:
out.append(groups[d2])
i += 3
continue
if 1 <= d1 <= num_groups:
out.append(groups[d1])
i += 2
else:
out.append("$" + nxt)
i += 2
else:
out.append("$")
i += 1
return "".join(out)
def find_replace(
input: str,
find: str,
replacement: str,
opts: Optional[Options] = None,
) -> FindReplaceResult:
"""Replace occurrences of ``find`` with ``replacement``. Never throws."""
o = opts or Options()
if find == "":
return FindReplaceResult(result=input, matches=0, error=None)
built = _build_regex(find, o)
if isinstance(built, str):
return FindReplaceResult(result=input, matches=0, error=built)
pattern = built
num_groups = pattern.groups # explicit capture-group count (an int)
matched = pattern.search(input) is not None
def repl(m: "re.Match") -> str:
groups = [m.group(0)] + [
(m.group(n) or "") for n in range(1, num_groups + 1)
]
return _expand(replacement, groups[0], groups, num_groups)
# count=0 -> replace all (global); count=1 -> first match only.
result = pattern.sub(repl, input, count=0 if o.global_ else 1)
if not matched:
matches = 0
elif o.global_:
matches = len(pattern.findall(input))
else:
matches = 1 + num_groups # JS String.match length quirk.
return FindReplaceResult(result=result, matches=matches, error=None)
if __name__ == "__main__":
r = find_replace(
"Hello World world", "world", "Universe",
Options(case_sensitive=False),
)
if r.error is not None:
print(f"error: {r.error}")
else:
print(f"{r.result} ({r.matches} matches)")
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →