Skip to content

Regex-Treffer per Callback-Funktion ersetzen snippet

Jeden Treffer durch das Resultat einer Funktion ersetzen — das Muster hinter Template-Engines, Syntax-Hervorhebung und Escaping.

Jeden Treffer durch das Resultat einer Funktion ersetzen — das Muster hinter Template-Engines, Syntax-Hervorhebung und Escaping. Die Divergenz ist die Signatur des Callbacks: JavaScript übergibt das ganze Match-Objekt (mit Gruppen und Index), Python das Match, PHP ein Array von Gruppen, Javas appendReplacement arbeitet auf einem StringBuffer mit expliziten Group-Aufrufen, Go nutzt ein func([]string) string. Die wiederkehrende Falle ist der Ersetzungsstring: '$&' und '\1' sind Metazeichen in den STRING-Formen — die Callback-Form existiert genau dazu, sie wieder zu Literalen zu machen.

Runnable recipe · 11 languagesTool regex-tester öffnen →
Regex & Text Processingregexreplacecallbacktext-processingtemplate-engine

Every language

11 languages, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
const lookup = { name: 'CosmoDev', page: 'docs' };

// Callback args: (fullMatch, ...groups, offset, wholeString) — arg 0 is
// ALWAYS the whole match, groups follow, so `name` here is group 1.
const out = 'Hello, [name]! See [page].'.replace(
  /\[(\w+)\]/g,
  (_, name) => lookup[name] ?? '[?]',
);
// 'Hello, CosmoDev! See docs.'

The callback's first argument is the full match, then each capture group, then offset and the whole string — which is why (_, name) => works. The equivalent STRING replacement would interpolate $& (whole match) and $1 (group 1) into the output; the callback's return value is inserted verbatim, no escaping.

TSTypeScript
type ReplaceFn = (substring: string, ...args: any[]) => string;

const lookup: Record<string, string> = { name: 'CosmoDev' };

export function interpolate(tpl: string): string {
  return tpl.replace(
    /\[(?<key>\w+)\]/gu,
    ((_m, ...rest: unknown[]) => {
      // arg 0 full match, then groups, offset, whole string — the
      // NAMED-groups bag arrives as the FINAL argument.
      const named = rest.at(-1) as Record<string, string | undefined>;
      return lookup[named.key ?? ''] ?? '[?]';
    }) satisfies ReplaceFn,
  );
}

interpolate('Hello, [name]!'); // 'Hello, CosmoDev!'

The stdlib types the callback as (substring: string, ...args: any[]) => string — everything after the full match is untyped. With named groups the bag is the LAST argument (after offset and the whole string), so rest.at(-1) is the only reliable read. satisfies ReplaceFn proves assignability while keeping the lambda's inference.

GoGo
import (
	"regexp"
	"strings"
)

var kebab = regexp.MustCompile(`-(\w)`)

// ReplaceAllStringFunc passes the callback ONLY the full match — no groups.
func kebabToCamel(s string) string {
	return kebab.ReplaceAllStringFunc(s, func(m string) string {
		// m is "-c" — re-find the group by running the regex on the match
		sub := kebab.FindStringSubmatch(m) // sub[0] full, sub[1] group 1
		return strings.ToUpper(sub[1])
	})
}

kebabToCamel("background-color-loading") // "backgroundColorLoading"

Go is the one major language whose callback gets NO match object: ReplaceAllStringFunc hands you a flat func(m string) string — the full match, nothing else. The documented workaround is re-running FindStringSubmatch(m) inside the callback to recover the groups. When no computation is needed, ReplaceAllString with ${1} skips the callback entirely.

RsRust
use regex::{Captures, Regex};

fn kebab_to_camel(s: &str) -> String {
    let re = Regex::new(r"-(\w)").unwrap();
    re.replace_all(s, |caps: &Captures| {
        // caps[0] full match, caps.get(1) group — no $-template rules here
        caps.get(1).map(|m| m.as_str().to_uppercase()).unwrap_or_default()
    })
    .into_owned()
}

assert_eq!(kebab_to_camel("background-color-loading"), "backgroundColorLoading");

replace_all takes a &str — where $1 and ${name} interpolate and a literal $ needs $$ — or a closure over &Captures, which reads caps[0] (full), caps.get(1), caps.name("g") with zero template rules. When the replacement is data (a path, user text), the closure form is mandatory: the template form would misread every $ and backslash in it. replace_all returns Cow<str>; into_owned() allocates only when something changed.

PHPPHP
$camel = preg_replace_callback(
    '/-(\w)/',
    fn($m) => strtoupper($m[1]),
    'background-color-loading',
);
// 'backgroundColorLoading'

// $m[0] is the full match ('-c'), $m[1] the group ('c'),
// $m['name'] for (?<name>...) groups — one array holds all three.

The callback receives ONE array: $m[0] full match, $m[1..N] groups, $m['name'] named — this is the kebab→camel one-liner. The STRING form of preg_replace processes \1 (and ${1}) in the replacement; the callback's return value goes in raw. On failure the function returns null, not the input — check !== null before trusting the result.

PyPython
import re

def kebab_to_camel(s: str) -> str:
    return re.sub(r'-(\w)', lambda m: m.group(1).upper(), s)

kebab_to_camel('background-color-loading')
# 'backgroundColorLoading'

# m.group(0) is the full match ('-c'), m.group(1) the group ('c').

re.sub's repl as a STRING interpolates \1 and \g<name> — and raises 'bad escape' on unknown sequences (a literal C:\Users path dies as \U). The lambda form gets the Match object — m.group(0) full, m.group(1) first group — and inserts its return value with no escape processing at all.

C#C#
using System.Text.RegularExpressions;

var camel = Regex.Replace(
    "background-color-loading",
    @"-(\w)",
    (Match m) => m.Groups[1].Value.ToUpperInvariant());
// backgroundColorLoading

// String form: Regex.Replace(s, @"-(\w)", "$1") — $1 and ${name}
// interpolate, $$ is a literal dollar. The MatchEvaluator skips all of it.

Regex.Replace takes a replacement STRING ($1, ${name}, $$ to escape) or a MatchEvaluator delegate — m.Groups[1].Value positional, m.Groups["name"].Value named. Only the delegate inserts computed output verbatim. ToUpperInvariant, not ToUpper(): a Turkish culture turns -i into İ and corrupts the identifier.

JvJava
import java.util.regex.*;

static String kebabToCamel(String s) {
    Matcher m = Pattern.compile("-(\\w)").matcher(s);
    StringBuilder sb = new StringBuilder();
    while (m.find()) {
        // group(1) is readable INSIDE the loop; appendReplacement parses
        // $ and \ in its argument — quote untrusted data.
        m.appendReplacement(sb, m.group(1).toUpperCase());
    }
    m.appendTail(sb);
    return sb.toString();
}

kebabToCamel("background-color-loading"); // "backgroundColorLoading"

Java has no callback replace — the idiom is the find() loop with appendReplacement into a StringBuilder plus appendTail. appendReplacement PARSES its second argument for $ and \\ (group refs): a stray $ in the data throws IllegalArgumentException mid-loop; Matcher.quoteReplacement(str) escapes it. Java 9+ takes StringBuilder; the StringBuffer overload is the legacy spelling.

SwSwift
import Foundation

func kebabToCamel(_ s: String) -> String {
    let re = try NSRegularExpression(pattern: "-(\\w)")
    let ns = s as NSString
    let out = NSMutableString(string: s)
    // Replace in REVERSE: each edit shifts every later NSRange.
    for m in re.matches(in: s, range: NSRange(location: 0, length: ns.length)).reversed() {
        let group = ns.substring(with: m.range(at: 1))
        out.replaceCharacters(in: m.range, with: group.uppercased())
    }
    return out as String
}

kebabToCamel("background-color-loading") // "backgroundColorLoading"

Swift's gap: the stdlib Regex API (5.7+) matches with typed captures but its replace is template-string based — there is no closure replace. The callback recipe still lives in Foundation: NSRegularExpression + matches(in:) with range(at: 1) per group, edited on a NSMutableString in REVERSE order because every replaceCharacters(in:) invalidates the ranges after it. Forgetting .reversed() is this language's version of the trap.

KtKotlin
val re = Regex("-(\\w)")

val camel = re.replace("background-color-loading") {
    it.groupValues[1].uppercase() // groupValues[0] is the whole match
}
// backgroundColorLoading

// The lambda overload takes (MatchResult) -> CharSequence — no $ escaping.
re.replace("a-b", "$1") // the STRING overload DOES interpolate $1

Regex.replace(input) { ... } is the closure form — a (MatchResult) -> CharSequence lambda replacing Java's appendReplacement loop, with no $ or \\ rules to remember. Inside it, groupValues is a List<String> whose index 0 is the whole match (always present, never null) and groupValues[1] the first group; groups[1] is the same slot as a nullable MatchGroup. The STRING overload replace(input, "$1") interpolates — same method name, different trap.

RbRuby
camel = 'background-color-loading'.gsub(/-(\w)/) { Regexp.last_match(1).upcase }
# => "backgroundColorLoading"

# BLOCK form: the return value replaces the match VERBATIM — no \\ rules.
# 'a-b'.gsub(/-(\w)/, '-\1')        # string form: backref interpolation
# 'a-b'.gsub(/-(\w)/) { $1.upcase }  # $1 works too, but is global state

gsub has two faces: with a STRING replacement, '-\1' interpolates the backreference and a literal backslash must be doubled; with a BLOCK, the block's return value goes in verbatim — zero escaping, which is the whole point. Inside the block read the group with Regexp.last_match(1); $1 also works but is set by the most recent match anywhere, so it breaks under refactoring.

Keep going

Try the interactive regex-tester tool →Read the regex-tokens cheatsheet →