152 lines
4.9 KiB
C++
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
|