initial commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user