#include "sedpp/Replacement.h" #include #include namespace sedpp { // mode is a persistent \U/\L state; once is the one-character \u/\l override. static char applyCase(char ch, int mode, bool &once) { unsigned char uch = static_cast(ch); if (mode == 1 || once) { ch = static_cast(std::toupper(uch)); once = false; } else if (mode == -1) { ch = static_cast(std::tolower(uch)); } return ch; } std::string Replacement::expand(const std::string &replacement, const std::string &subject, const std::vector &groups) { std::string out; int caseMode = 0; // 0 unchanged, 1 upper, -1 lower. bool upperOnce = false; bool lowerOnce = false; // Append through one funnel so literal text, &, backreferences, and decoded // byte escapes all share the same case-conversion state machine. auto append = [&](std::string_view text) { for (char ch : text) { if (upperOnce) { bool once = true; out.push_back(applyCase(ch, 1, once)); upperOnce = false; } else if (lowerOnce) { out.push_back(static_cast( std::tolower(static_cast(ch)))); lowerOnce = false; } else if (caseMode == 1) { out.push_back(static_cast( std::toupper(static_cast(ch)))); } else if (caseMode == -1) { out.push_back(static_cast( std::tolower(static_cast(ch)))); } else { out.push_back(ch); } } }; for (std::size_t i = 0; i < replacement.size(); ++i) { const char ch = replacement[i]; if (ch == '&') { // & expands to the whole match. const regmatch_t &whole = groups[0]; append(std::string_view(subject).substr(whole.rm_so, whole.rm_eo - whole.rm_so)); continue; } if (ch != '\\' || i + 1 >= replacement.size()) { append(std::string_view(&replacement[i], 1)); continue; } const char next = replacement[++i]; if (next >= '0' && next <= '9') { // Missing capture groups expand to the empty string. const int group = next - '0'; if (group < static_cast(groups.size()) && groups[group].rm_so >= 0) { append(std::string_view(subject).substr( groups[group].rm_so, groups[group].rm_eo - groups[group].rm_so)); } } else if (next == 'U') { caseMode = 1; } else if (next == 'L') { caseMode = -1; } else if (next == 'u') { upperOnce = true; } else if (next == 'l') { lowerOnce = true; } else if (next == 'E') { caseMode = 0; upperOnce = false; lowerOnce = false; } else if (next == 'n') { append("\n"); } else if (next == 't') { append("\t"); } else if (next == 'd' || next == 'o' || next == 'x') { // GNU sed accepts decimal, octal, and hexadecimal byte escapes on the RHS // of substitutions. const int base = next == 'd' ? 10 : (next == 'o' ? 8 : 16); const int limit = next == 'x' ? 2 : 3; const auto hexValue = [](char c) -> int { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; }; int value = 0; int consumed = 0; while (i + 1 < replacement.size() && consumed < limit) { const int digit = hexValue(replacement[i + 1]); if (digit < 0 || digit >= base) { break; } value = value * base + digit; ++i; ++consumed; } if (consumed > 0) { const char valueChar = static_cast(value & 0xff); append(std::string_view(&valueChar, 1)); } else { const char literal = next; append(std::string_view(&literal, 1)); } } else if (next == 'a') { append("\a"); } else if (next == 'f') { append("\f"); } else if (next == 'r') { append("\r"); } else if (next == 'v') { append("\v"); } else if (next == 'c') { if (i + 1 >= replacement.size()) { append("\\"); continue; } // \cX maps X into the control-character range. A doubled backslash after // \c is consumed so recursive escaping does not leak into output. const unsigned char raw = static_cast(replacement[++i]); const char control = std::isalpha(raw) ? static_cast(std::toupper(raw) & 0x1f) : static_cast(raw ^ 0x40); if (raw == '\\' && i + 1 < replacement.size() && replacement[i + 1] == '\\') { ++i; } append(std::string_view(&control, 1)); } else { append(std::string_view(&next, 1)); } } return out; } } // namespace sedpp