Files
SedPP/src/Replacement.cpp
2026-06-09 00:22:22 -05:00

152 lines
4.9 KiB
C++

#include "sedpp/Replacement.h"
#include <cctype>
#include <string_view>
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<unsigned char>(ch);
if (mode == 1 || once) {
ch = static_cast<char>(std::toupper(uch));
once = false;
} else if (mode == -1) {
ch = static_cast<char>(std::tolower(uch));
}
return ch;
}
std::string Replacement::expand(const std::string &replacement,
const std::string &subject,
const std::vector<regmatch_t> &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<char>(
std::tolower(static_cast<unsigned char>(ch))));
lowerOnce = false;
} else if (caseMode == 1) {
out.push_back(static_cast<char>(
std::toupper(static_cast<unsigned char>(ch))));
} else if (caseMode == -1) {
out.push_back(static_cast<char>(
std::tolower(static_cast<unsigned char>(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<int>(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<char>(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<unsigned char>(replacement[++i]);
const char control = std::isalpha(raw)
? static_cast<char>(std::toupper(raw) & 0x1f)
: static_cast<char>(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