Skip to content

Remplacer les correspondances regex par une fonction callback snippet

Remplacer chaque match par le résultat d'une fonction — le motif derrière les moteurs de templates, la coloration syntaxique et l'échappement.

Remplacer chaque match par le résultat d'une fonction — le motif derrière les moteurs de templates, la coloration syntaxique et l'échappement. La divergence est la signature du callback : JavaScript passe l'objet match entier (avec les groupes et l'index), Python passe le match, PHP un tableau de groupes, le appendReplacement de Java travaille sur un StringBuffer avec des appels de groupe explicites, Go utilise un func([]string) string. Le piège récurrent est la chaîne de remplacement : '$&' et '\1' sont des métacaractères dans les formes STRING — la forme callback existe précisément pour les transformer en littéraux.

Recette exécutable · 11 langagesOpen the regex-tester tool →
Regex & Text Processingregexreplacecallbacktext-processingtemplate-engine

Every language

11 langages, 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 →