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 $1Regex.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 stategsub 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 →