Find & Replace — C++ 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 C++ 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: C++ (C++17, standard library only — std::regex's ECMAScript
// grammar is the JS regex syntax; the multiline flag needs C++17)
// Source: CosmoDev polyglot showcase port of the find-replace tool,
// ported from src/lib/findReplace.ts (the canonical TypeScript
// implementation).
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Mirrors the live lib: a literal find string is regex-escaped and matched
// verbatim; an isRegex find is compiled as-is. `\b` wraps the pattern when
// wholeWord is set, and std::regex composes icase and multiline (the JS
// i / m flags). std::regex_error is caught and returned as the error
// string — the port never throws — and an empty find is a no-op.
//
// Replacement $-substitution is implemented in expand_match (not
// regex_replace, whose out-of-range $n handling differs) 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 $' are intentionally unsupported.
#include <cstddef>
#include <cstring>
#include <iostream>
#include <optional>
#include <regex>
#include <string>
#include <vector>
/// Options mirror the TypeScript lib's FindReplaceOptions field for field.
/// `global_` is suffixed to keep the parallel naming without clashing.
struct Options {
bool is_regex = false;
bool case_sensitive = true;
bool whole_word = false;
bool global_ = true;
bool multiline = false;
};
struct FindReplaceResult {
std::string result;
std::size_t matches = 0;
std::string error; // empty means none
};
/// Escape regex metacharacters so a literal find string matches verbatim.
std::string escape_regex(const std::string& s) {
static const char* META = ".^$*+?()[]{}|\\";
std::string out;
out.reserve(s.size() * 2);
for (char c : s) {
if (c != '\0' && std::strchr(META, c) != nullptr) {
out.push_back('\\');
}
out.push_back(c);
}
return out;
}
/// Compile the find expression with flag + whole-word modifiers. On
/// invalid syntax the engine's error string is returned (the lib's error
/// path); the port never throws.
std::optional<std::regex> build_regex(const std::string& find, const Options& o,
std::string* err) {
std::string pattern = o.is_regex ? find : escape_regex(find);
if (o.whole_word) {
pattern = "\\b" + pattern + "\\b";
}
auto flags = std::regex_constants::ECMAScript;
if (!o.case_sensitive) {
flags |= std::regex_constants::icase;
}
if (o.is_regex && o.multiline) {
flags |= std::regex_constants::multiline;
}
try {
return std::regex(pattern, flags);
} catch (const std::regex_error& e) {
*err = e.what();
return std::nullopt;
}
}
bool is_ascii_digit(char c) { return c >= '0' && c <= '9'; }
/// 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).
/// groups[0] is the whole match; unmatched groups are "".
std::string expand_match(const std::string& tpl, const std::vector<std::string>& groups,
std::size_t num_groups) {
std::string out;
std::size_t i = 0;
while (i < tpl.size()) {
char c = tpl[i];
if (c != '$') {
out.push_back(c);
++i;
continue;
}
char n = (i + 1 < tpl.size()) ? tpl[i + 1] : '\0';
if (n == '$') {
out.push_back('$');
i += 2;
} else if (n == '&') {
out += groups[0];
i += 2;
} else if (is_ascii_digit(n)) {
std::size_t d1 = static_cast<std::size_t>(n - '0');
// Greedily try a second digit ($nn), matching JS.
if (i + 2 < tpl.size() && is_ascii_digit(tpl[i + 2])) {
std::size_t d2 = d1 * 10 + static_cast<std::size_t>(tpl[i + 2] - '0');
if (d2 >= 1 && d2 <= num_groups) {
out += groups[d2];
i += 3;
continue;
}
}
if (d1 >= 1 && d1 <= num_groups) {
out += groups[d1];
i += 2;
} else {
out.push_back('$');
out.push_back(n);
i += 2;
}
} else {
out.push_back('$');
++i;
}
}
return out;
}
FindReplaceResult find_replace(const std::string& input, const std::string& find,
const std::string& replacement, Options o = {}) {
if (find.empty()) {
return {input, 0, ""}; // empty find is a no-op
}
std::string err;
auto built = build_regex(find, o, &err);
if (!built) {
return {input, 0, err};
}
const std::regex& re = *built;
const std::size_t num_groups = re.mark_count();
std::string out;
std::size_t matches = 0;
std::size_t last = 0;
// sregex_iterator guarantees forward progress on empty matches.
for (auto it = std::sregex_iterator(input.begin(), input.end(), re);
it != std::sregex_iterator(); ++it) {
const std::smatch m = *it;
std::size_t pos = static_cast<std::size_t>(m.position());
std::size_t len = static_cast<std::size_t>(m.length());
out += input.substr(last, pos - last);
std::vector<std::string> groups(num_groups + 1);
for (std::size_t g = 0; g <= num_groups; ++g) {
groups[g] = m[g].matched ? m[g].str() : "";
}
out += expand_match(replacement, groups, num_groups);
last = pos + len;
++matches;
if (!o.global_) {
break;
}
}
out += input.substr(last);
if (matches != 0 && !o.global_) {
matches = 1 + num_groups; // JS String.match length quirk
}
return {out, matches, ""};
}
int main() {
Options o;
o.case_sensitive = false;
FindReplaceResult r = find_replace("Hello World world", "world", "Universe", o);
if (!r.error.empty()) {
std::cerr << "error: " << r.error << '\n';
} else {
std::cout << r.result << " (" << r.matches << " matches)\n";
}
return 0;
}
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 →