Skip to content

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 →