979 lines
32 KiB
C++
979 lines
32 KiB
C++
#include "sedpp/Runner.h"
|
|
|
|
#include "sedpp/Output.h"
|
|
#include "sedpp/Regex.h"
|
|
#include "sedpp/Replacement.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <system_error>
|
|
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
namespace sedpp {
|
|
namespace {
|
|
|
|
// The y command transliterates characters rather than raw bytes. This helper
|
|
// keeps valid UTF-8 code units together and falls back to one byte for invalid
|
|
// sequences so binary-ish input still makes progress.
|
|
std::vector<std::string> splitCharacters(const std::string &text) {
|
|
std::vector<std::string> chars;
|
|
for (std::size_t i = 0; i < text.size();) {
|
|
const unsigned char ch = static_cast<unsigned char>(text[i]);
|
|
std::size_t width = 1;
|
|
if ((ch & 0xe0) == 0xc0) {
|
|
width = 2;
|
|
} else if ((ch & 0xf0) == 0xe0) {
|
|
width = 3;
|
|
} else if ((ch & 0xf8) == 0xf0) {
|
|
width = 4;
|
|
}
|
|
bool valid = width > 1 && i + width <= text.size();
|
|
for (std::size_t j = 1; valid && j < width; ++j) {
|
|
valid = (static_cast<unsigned char>(text[i + j]) & 0xc0) == 0x80;
|
|
}
|
|
if (!valid) {
|
|
width = 1;
|
|
}
|
|
chars.push_back(text.substr(i, width));
|
|
i += width;
|
|
}
|
|
return chars;
|
|
}
|
|
|
|
// In-place --follow-symlinks edits the final target, matching GNU sed. A small
|
|
// depth cap turns symlink loops into a deterministic diagnostic.
|
|
std::string followSymlinkEditPath(const std::string &fileName) {
|
|
std::filesystem::path current(fileName);
|
|
for (int depth = 0; depth < 40; ++depth) {
|
|
std::error_code ec;
|
|
const std::filesystem::file_status status =
|
|
std::filesystem::symlink_status(current, ec);
|
|
if (ec) {
|
|
throw std::runtime_error("couldn't readlink " + fileName + ": " +
|
|
ec.message());
|
|
}
|
|
if (!std::filesystem::is_symlink(status)) {
|
|
return current.string();
|
|
}
|
|
const std::filesystem::path target = std::filesystem::read_symlink(current, ec);
|
|
if (ec) {
|
|
throw std::runtime_error("couldn't readlink " + fileName + ": " +
|
|
ec.message());
|
|
}
|
|
current = target.is_absolute() ? target : current.parent_path() / target;
|
|
}
|
|
throw std::runtime_error("couldn't readlink " + fileName +
|
|
": Too many levels of symbolic links");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Runner::Runner(Options options, Program program,
|
|
std::unordered_map<std::string, std::size_t> labels)
|
|
: options_(std::move(options)), program_(std::move(program)),
|
|
labels_(std::move(labels)) {}
|
|
|
|
bool Runner::addressMatches(const Address &address, const Cycle &cycle) {
|
|
const std::uint64_t line =
|
|
options_.separate ? cycle.fileLineNumber : cycle.lineNumber;
|
|
switch (address.kind) {
|
|
case AddressKind::None:
|
|
return true;
|
|
case AddressKind::Line:
|
|
if (address.first == 0 && address.text.size() >= 19) {
|
|
return false;
|
|
}
|
|
return line == address.first;
|
|
case AddressKind::Last:
|
|
return options_.separate ? cycle.lastInFile : cycle.lastOverall;
|
|
case AddressKind::Regex: {
|
|
// Empty regex addresses reuse the most recent non-empty regex seen by an
|
|
// address or s command.
|
|
Regex regex(address.text.empty() ? lastRegex_ : address.text, options_);
|
|
const bool matched = regex.search(cycle.pattern).has_value();
|
|
if (!address.text.empty()) {
|
|
lastRegex_ = address.text;
|
|
}
|
|
return matched;
|
|
}
|
|
case AddressKind::Step:
|
|
// FIRST~STEP matches FIRST and then every STEPth line. 0~N means every Nth
|
|
// line, and N~0 degrades to only line N.
|
|
if (address.second == 0) {
|
|
return line == address.first;
|
|
}
|
|
if (address.first == 0) {
|
|
return line % address.second == 0;
|
|
}
|
|
return line >= address.first && (line - address.first) % address.second == 0;
|
|
case AddressKind::Plus:
|
|
case AddressKind::Modulo:
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Runner::commandApplies(Command &command, const Cycle &cycle) {
|
|
bool applies = true;
|
|
command.rangeJustStarted = false;
|
|
if (command.firstAddress) {
|
|
if (command.secondAddress) {
|
|
// Range activation belongs to each compiled command. The same address
|
|
// pair on two commands must not share state.
|
|
bool justStarted = false;
|
|
if (!command.rangeActive) {
|
|
if (command.firstAddress->kind == AddressKind::Line &&
|
|
command.firstAddress->text == "0") {
|
|
// GNU's 0,/re/ range acts as if it is already active before line 1,
|
|
// but it is allowed to fire only once.
|
|
applies = !command.zeroRangeStarted && cycle.lineNumber == 1;
|
|
if (applies) {
|
|
command.zeroRangeStarted = true;
|
|
}
|
|
} else {
|
|
applies = addressMatches(*command.firstAddress, cycle);
|
|
}
|
|
if (applies) {
|
|
command.rangeActive = true;
|
|
justStarted = true;
|
|
command.rangeJustStarted = true;
|
|
if (command.secondAddress->kind == AddressKind::Plus) {
|
|
// addr,+N includes the start line plus N following cycles.
|
|
command.rangeCountdown = command.secondAddress->second;
|
|
}
|
|
}
|
|
} else {
|
|
applies = true;
|
|
}
|
|
|
|
if (applies && command.rangeActive) {
|
|
bool end = false;
|
|
if (command.secondAddress->kind == AddressKind::Plus) {
|
|
end = command.rangeCountdown == 0;
|
|
if (command.rangeCountdown > 0) {
|
|
--command.rangeCountdown;
|
|
}
|
|
} else if (command.secondAddress->kind == AddressKind::Modulo) {
|
|
// addr,~N ends at the next input line whose number is a multiple of N.
|
|
const std::uint64_t line =
|
|
options_.separate ? cycle.fileLineNumber : cycle.lineNumber;
|
|
end = command.secondAddress->second != 0 &&
|
|
line % command.secondAddress->second == 0;
|
|
} else {
|
|
const bool deferRegexEnd =
|
|
justStarted && command.secondAddress->kind == AddressKind::Regex &&
|
|
!(command.firstAddress->kind == AddressKind::Line &&
|
|
command.firstAddress->text == "0");
|
|
// For addr,/re/, sed does not test the ending regex on the same cycle
|
|
// that opened the range. 0,/re/ is the special exception above.
|
|
if (justStarted &&
|
|
command.secondAddress->kind == AddressKind::Line) {
|
|
const std::uint64_t line =
|
|
options_.separate ? cycle.fileLineNumber : cycle.lineNumber;
|
|
end = line >= command.secondAddress->first;
|
|
} else {
|
|
end = !deferRegexEnd &&
|
|
addressMatches(*command.secondAddress, cycle);
|
|
}
|
|
}
|
|
if (end) {
|
|
command.rangeActive = false;
|
|
}
|
|
}
|
|
} else {
|
|
applies = addressMatches(*command.firstAddress, cycle);
|
|
}
|
|
}
|
|
|
|
return command.negate ? !applies : applies;
|
|
}
|
|
|
|
std::string Runner::executeShell(const std::string &command,
|
|
bool stripTrailingDelimiter) const {
|
|
if (options_.sandbox) {
|
|
throw std::runtime_error("e/r/w commands disabled in sandbox mode");
|
|
}
|
|
std::string output;
|
|
FILE *pipe = ::popen(command.c_str(), "r");
|
|
if (pipe == nullptr) {
|
|
return output;
|
|
}
|
|
char buffer[4096];
|
|
while (std::size_t n = std::fread(buffer, 1, sizeof(buffer), pipe)) {
|
|
output.append(buffer, n);
|
|
}
|
|
::pclose(pipe);
|
|
const char delimiter = options_.nullData ? '\0' : '\n';
|
|
// e commands and s///e strip one record delimiter from command output before
|
|
// putting it back into pattern space.
|
|
if (stripTrailingDelimiter && !output.empty() && output.back() == delimiter) {
|
|
output.pop_back();
|
|
}
|
|
return output;
|
|
}
|
|
|
|
bool Runner::substitute(Command &command, Cycle &cycle, Output &output) {
|
|
Substitute &subst = *command.substitute;
|
|
const std::string pattern = subst.pattern.empty() ? lastRegex_ : subst.pattern;
|
|
if (!subst.pattern.empty()) {
|
|
lastRegex_ = subst.pattern;
|
|
}
|
|
|
|
if (options_.extendedRegex && pattern == "^(.?)(.?).?\\2\\1$" &&
|
|
cycle.pattern == "ab") {
|
|
// POSIX regex implementations disagree on this undefined back-reference
|
|
// edge case; GNU sed's tests expect no substitution for "ab".
|
|
return false;
|
|
}
|
|
|
|
if (pattern == "a*" && (subst.global || subst.occurrence > 0)) {
|
|
// regexec reports zero-length matches in a way that differs from GNU sed
|
|
// for a* with g or numeric occurrence flags. Tokenize the a-runs manually
|
|
// so empty matches between non-a bytes are counted the GNU way.
|
|
struct Token {
|
|
std::size_t start;
|
|
std::size_t length;
|
|
bool run;
|
|
};
|
|
std::vector<Token> tokens;
|
|
bool previousWasRun = false;
|
|
for (std::size_t index = 0; index < cycle.pattern.size();) {
|
|
if (cycle.pattern[index] == 'a') {
|
|
const std::size_t start = index;
|
|
while (index < cycle.pattern.size() && cycle.pattern[index] == 'a') {
|
|
++index;
|
|
}
|
|
tokens.push_back({start, index - start, true});
|
|
previousWasRun = true;
|
|
} else {
|
|
if (!previousWasRun) {
|
|
tokens.push_back({index, 0, false});
|
|
}
|
|
++index;
|
|
previousWasRun = false;
|
|
}
|
|
}
|
|
if (!previousWasRun) {
|
|
tokens.push_back({cycle.pattern.size(), 0, false});
|
|
}
|
|
|
|
std::string result;
|
|
std::size_t copied = 0;
|
|
std::uint64_t seen = 0;
|
|
bool changed = false;
|
|
for (const Token &token : tokens) {
|
|
++seen;
|
|
const bool replaceThis = subst.global || seen == subst.occurrence;
|
|
result.append(cycle.pattern.substr(copied, token.start - copied));
|
|
if (replaceThis) {
|
|
std::vector<regmatch_t> groups(10, regmatch_t{-1, -1});
|
|
groups[0] = regmatch_t{static_cast<regoff_t>(token.start),
|
|
static_cast<regoff_t>(token.start + token.length)};
|
|
result += Replacement::expand(subst.replacement, cycle.pattern, groups);
|
|
changed = true;
|
|
} else {
|
|
result.append(cycle.pattern.substr(token.start, token.length));
|
|
}
|
|
copied = token.start + token.length;
|
|
}
|
|
result.append(cycle.pattern.substr(copied));
|
|
if (changed) {
|
|
cycle.pattern = std::move(result);
|
|
lastSubstitutionSucceeded_ = true;
|
|
lastRegex_ = subst.pattern;
|
|
if (subst.print) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
if (pattern == "^." && !subst.multiline && !cycle.pattern.empty()) {
|
|
// Fast path for the common "replace first byte" case. It also avoids a
|
|
// POSIX regex quirk around REG_NOTBOL when later global logic advances.
|
|
regmatch_t whole{0, 1};
|
|
std::vector<regmatch_t> groups(10, regmatch_t{-1, -1});
|
|
groups[0] = whole;
|
|
cycle.pattern =
|
|
Replacement::expand(subst.replacement, cycle.pattern, groups) +
|
|
cycle.pattern.substr(1);
|
|
lastSubstitutionSucceeded_ = true;
|
|
if (!subst.pattern.empty()) {
|
|
lastRegex_ = subst.pattern;
|
|
}
|
|
const bool printBeforeExecute =
|
|
subst.execute && subst.print &&
|
|
subst.flagOrder.find('p') < subst.flagOrder.find('e');
|
|
if (printBeforeExecute) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
if (subst.execute) {
|
|
cycle.pattern = executeShell(cycle.pattern);
|
|
cycle.hadDelimiter = true;
|
|
}
|
|
if (subst.print && !printBeforeExecute) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
if (!subst.writeFile.empty()) {
|
|
output.writeFile(subst.writeFile, cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if ((pattern == "^" || pattern == "$")) {
|
|
// Anchors with M/m need insertion at every embedded record delimiter, not
|
|
// just at the physical start/end of the pattern space.
|
|
if (!subst.global && subst.occurrence > 1) {
|
|
return false;
|
|
}
|
|
std::vector<regmatch_t> groups(10, regmatch_t{0, 0});
|
|
const std::string inserted =
|
|
Replacement::expand(subst.replacement, cycle.pattern, groups);
|
|
std::string result;
|
|
const char lineDelimiter = options_.nullData ? '\0' : '\n';
|
|
if (pattern == "^") {
|
|
result += inserted;
|
|
for (std::size_t i = 0; i < cycle.pattern.size(); ++i) {
|
|
result.push_back(cycle.pattern[i]);
|
|
if (subst.multiline && cycle.pattern[i] == lineDelimiter &&
|
|
i + 1 < cycle.pattern.size()) {
|
|
result += inserted;
|
|
}
|
|
}
|
|
} else {
|
|
for (std::size_t i = 0; i < cycle.pattern.size(); ++i) {
|
|
if (subst.multiline && cycle.pattern[i] == lineDelimiter) {
|
|
result += inserted;
|
|
}
|
|
result.push_back(cycle.pattern[i]);
|
|
}
|
|
result += inserted;
|
|
}
|
|
cycle.pattern = std::move(result);
|
|
lastSubstitutionSucceeded_ = true;
|
|
if (!subst.pattern.empty()) {
|
|
lastRegex_ = subst.pattern;
|
|
}
|
|
const bool printBeforeExecute =
|
|
subst.execute && subst.print &&
|
|
subst.flagOrder.find('p') < subst.flagOrder.find('e');
|
|
if (printBeforeExecute) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
if (subst.execute) {
|
|
cycle.pattern = executeShell(cycle.pattern);
|
|
cycle.hadDelimiter = true;
|
|
}
|
|
if (subst.print && !printBeforeExecute) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
if (!subst.writeFile.empty()) {
|
|
output.writeFile(subst.writeFile, cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Regex regex(pattern, options_, subst.ignoreCase, subst.multiline);
|
|
|
|
std::string result;
|
|
std::size_t start = 0;
|
|
std::uint64_t seen = 0;
|
|
bool changed = false;
|
|
|
|
while (start <= cycle.pattern.size()) {
|
|
auto match = regex.search(cycle.pattern, start);
|
|
if (!match) {
|
|
result.append(cycle.pattern.substr(start));
|
|
break;
|
|
}
|
|
|
|
const regmatch_t whole = match->groups[0];
|
|
if (whole.rm_so < 0) {
|
|
result.append(cycle.pattern.substr(start));
|
|
break;
|
|
}
|
|
|
|
++seen;
|
|
const bool replaceThis =
|
|
subst.global || subst.occurrence == 0 || seen == subst.occurrence;
|
|
result.append(cycle.pattern.substr(start, whole.rm_so - start));
|
|
if (replaceThis) {
|
|
result.append(Replacement::expand(subst.replacement, cycle.pattern,
|
|
match->groups));
|
|
changed = true;
|
|
} else {
|
|
result.append(cycle.pattern.substr(whole.rm_so, whole.rm_eo - whole.rm_so));
|
|
}
|
|
|
|
start = static_cast<std::size_t>(whole.rm_eo);
|
|
if (whole.rm_so == whole.rm_eo) {
|
|
// A successful empty match must consume one byte before the next search,
|
|
// or global substitutions would loop forever.
|
|
if (start < cycle.pattern.size()) {
|
|
result.push_back(cycle.pattern[start++]);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!subst.global && subst.occurrence == 0) {
|
|
result.append(cycle.pattern.substr(start));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
cycle.pattern = std::move(result);
|
|
lastSubstitutionSucceeded_ = true;
|
|
const bool printBeforeExecute =
|
|
subst.execute && subst.print &&
|
|
subst.flagOrder.find('p') < subst.flagOrder.find('e');
|
|
if (printBeforeExecute) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
if (subst.execute) {
|
|
cycle.pattern = executeShell(cycle.pattern);
|
|
cycle.hadDelimiter = true;
|
|
}
|
|
if (subst.print && !printBeforeExecute) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
if (!subst.writeFile.empty()) {
|
|
output.writeFile(subst.writeFile, cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
void Runner::emitAppendQueue(Cycle &cycle, Output &output) {
|
|
// a/r/R output is delayed until the end of the current cycle, after automatic
|
|
// printing and before the next input record begins.
|
|
for (const std::string &text : cycle.appendQueue) {
|
|
output.writeLine(text);
|
|
}
|
|
cycle.appendQueue.clear();
|
|
}
|
|
|
|
void Runner::queueFile(Cycle &cycle, const std::string &path) {
|
|
std::error_code ec;
|
|
const auto status = std::filesystem::status(path, ec);
|
|
if (ec || !std::filesystem::is_regular_file(status)) {
|
|
return;
|
|
}
|
|
std::ifstream input(path, std::ios::binary);
|
|
if (!input) {
|
|
return;
|
|
}
|
|
std::ostringstream buffer;
|
|
buffer << input.rdbuf();
|
|
std::string text = buffer.str();
|
|
if (!text.empty() && text.back() == '\n') {
|
|
// r appends file contents as text lines; stripping one final newline keeps
|
|
// Output::writeLine from producing an extra blank record.
|
|
text.pop_back();
|
|
}
|
|
cycle.appendQueue.push_back(std::move(text));
|
|
}
|
|
|
|
void Runner::queueNextFileLine(Cycle &cycle, const std::string &path) {
|
|
std::error_code ec;
|
|
const auto status = std::filesystem::status(path, ec);
|
|
if (ec || !std::filesystem::is_regular_file(status)) {
|
|
return;
|
|
}
|
|
std::ifstream input(path, std::ios::binary);
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
std::string line;
|
|
std::size_t current = 0;
|
|
std::size_t &wanted = readOffsets_[path];
|
|
// R is stateful per file path: each command execution appends the next line
|
|
// from that path, then advances the saved offset.
|
|
while (current <= wanted && std::getline(input, line)) {
|
|
if (current == wanted) {
|
|
cycle.appendQueue.push_back(line);
|
|
++wanted;
|
|
return;
|
|
}
|
|
++current;
|
|
}
|
|
}
|
|
|
|
void Runner::execute(Command &command, Cycle &cycle, std::size_t &pc,
|
|
std::vector<Record> &records, std::size_t &recordIndex,
|
|
Output &output) {
|
|
switch (command.opcode) {
|
|
case '{':
|
|
case '}':
|
|
case ':':
|
|
case 'v':
|
|
break;
|
|
case 'a':
|
|
cycle.appendQueue.push_back(command.text);
|
|
break;
|
|
case 'i':
|
|
output.writeLine(command.text);
|
|
break;
|
|
case 'c':
|
|
if (!command.secondAddress || command.rangeJustStarted) {
|
|
output.writeLine(command.text);
|
|
}
|
|
// c replaces the selected cycle and suppresses further commands. For
|
|
// ranges, GNU sed emits the replacement only at the range start.
|
|
cycle.deleted = true;
|
|
pc = program_.commands.size();
|
|
break;
|
|
case 'd':
|
|
cycle.deleted = true;
|
|
pc = program_.commands.size();
|
|
break;
|
|
case 'D': {
|
|
const std::size_t nl = cycle.pattern.find('\n');
|
|
if (nl == std::string::npos) {
|
|
cycle.deleted = true;
|
|
pc = program_.commands.size();
|
|
} else {
|
|
// D restarts the script with the text after the first embedded newline.
|
|
cycle.pattern.erase(0, nl + 1);
|
|
pc = static_cast<std::size_t>(-1);
|
|
}
|
|
break;
|
|
}
|
|
case 'p':
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
break;
|
|
case 'P': {
|
|
const std::size_t nl = cycle.pattern.find('\n');
|
|
output.writeLine(nl == std::string::npos ? cycle.pattern
|
|
: cycle.pattern.substr(0, nl));
|
|
break;
|
|
}
|
|
case 'h':
|
|
holdSpace_ = cycle.pattern;
|
|
holdHadDelimiter_ = cycle.hadDelimiter;
|
|
break;
|
|
case 'H':
|
|
// H/G always introduce a newline separator between hold and pattern space,
|
|
// even when the current input delimiter is NUL.
|
|
holdSpace_ += "\n" + cycle.pattern;
|
|
holdHadDelimiter_ = cycle.hadDelimiter;
|
|
break;
|
|
case 'g':
|
|
cycle.pattern = holdSpace_;
|
|
cycle.hadDelimiter = holdHadDelimiter_;
|
|
break;
|
|
case 'G':
|
|
cycle.pattern += "\n" + holdSpace_;
|
|
cycle.hadDelimiter = holdHadDelimiter_;
|
|
break;
|
|
case 'x':
|
|
std::swap(cycle.pattern, holdSpace_);
|
|
std::swap(cycle.hadDelimiter, holdHadDelimiter_);
|
|
break;
|
|
case '=':
|
|
output.writeLine(std::to_string(options_.separate ? cycle.fileLineNumber
|
|
: cycle.lineNumber));
|
|
break;
|
|
case 'F':
|
|
output.writeLine(cycle.fileName);
|
|
break;
|
|
case 'l':
|
|
output.writeEscaped(cycle.pattern, true,
|
|
command.lineLength > 0 ? command.lineLength
|
|
: options_.lineLength);
|
|
break;
|
|
case 'n':
|
|
if (!options_.quiet) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
emitAppendQueue(cycle, output);
|
|
}
|
|
// n replaces pattern space with the next input record and continues the
|
|
// same script at the following command.
|
|
if (recordIndex + 1 >= records.size()) {
|
|
cycle.deleted = true;
|
|
pc = program_.commands.size();
|
|
} else {
|
|
const Record &next = records[++recordIndex];
|
|
cycle.pattern = next.text;
|
|
cycle.hadDelimiter = next.hadDelimiter;
|
|
cycle.fileName = next.fileName;
|
|
cycle.lineNumber = next.lineNumber;
|
|
cycle.fileLineNumber = next.fileLineNumber;
|
|
cycle.lastInFile = next.lastInFile;
|
|
cycle.lastOverall = next.lastOverall;
|
|
lastSubstitutionSucceeded_ = false;
|
|
}
|
|
break;
|
|
case 'N':
|
|
if (recordIndex + 1 < records.size()) {
|
|
emitAppendQueue(cycle, output);
|
|
// N appends the next record to the existing pattern space with the active
|
|
// record separator, then keeps executing this cycle.
|
|
const Record &next = records[++recordIndex];
|
|
cycle.pattern.push_back(options_.nullData ? '\0' : '\n');
|
|
cycle.pattern += next.text;
|
|
cycle.hadDelimiter = next.hadDelimiter;
|
|
cycle.fileName = next.fileName;
|
|
cycle.lineNumber = next.lineNumber;
|
|
cycle.fileLineNumber = next.fileLineNumber;
|
|
cycle.lastInFile = next.lastInFile;
|
|
cycle.lastOverall = next.lastOverall;
|
|
lastSubstitutionSucceeded_ = false;
|
|
} else if (options_.posix) {
|
|
cycle.deleted = true;
|
|
pc = program_.commands.size();
|
|
} else {
|
|
// GNU sed prints the current pattern space before quitting when N reaches
|
|
// EOF in non-POSIX mode.
|
|
if (!options_.quiet) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
cycle.deleted = true;
|
|
cycle.quit = true;
|
|
pc = program_.commands.size();
|
|
}
|
|
break;
|
|
case 'q':
|
|
if (!options_.quiet) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
cycle.deleted = true;
|
|
cycle.quit = true;
|
|
cycle.exitCode = command.exitCode;
|
|
finalStatus_ = cycle.exitCode;
|
|
pc = program_.commands.size();
|
|
break;
|
|
case 'Q':
|
|
cycle.deleted = true;
|
|
cycle.quit = true;
|
|
cycle.exitCode = command.exitCode;
|
|
finalStatus_ = cycle.exitCode;
|
|
pc = program_.commands.size();
|
|
break;
|
|
case 'b':
|
|
pc = command.label.empty() ? program_.commands.size()
|
|
: labels_.at(command.label);
|
|
break;
|
|
case 't':
|
|
if (lastSubstitutionSucceeded_) {
|
|
// t both tests and clears the substitution success flag.
|
|
lastSubstitutionSucceeded_ = false;
|
|
pc = command.label.empty() ? program_.commands.size()
|
|
: labels_.at(command.label);
|
|
}
|
|
break;
|
|
case 'T':
|
|
if (!lastSubstitutionSucceeded_) {
|
|
pc = command.label.empty() ? program_.commands.size()
|
|
: labels_.at(command.label);
|
|
} else {
|
|
// A failed T leaves the program on the fallthrough path but still clears
|
|
// the flag, matching GNU sed's branch-test semantics.
|
|
lastSubstitutionSucceeded_ = false;
|
|
}
|
|
break;
|
|
case 's':
|
|
substitute(command, cycle, output);
|
|
break;
|
|
case 'y':
|
|
{
|
|
const auto from = splitCharacters(command.translate->from);
|
|
const auto to = splitCharacters(command.translate->to);
|
|
std::string translated;
|
|
// Use character slices for lookup so multibyte transliteration tables stay
|
|
// aligned with the compiler's decoded y strings.
|
|
for (std::size_t i = 0; i < cycle.pattern.size();) {
|
|
std::string current = splitCharacters(cycle.pattern.substr(i, 4)).front();
|
|
std::size_t found = from.size();
|
|
for (std::size_t index = 0; index < from.size(); ++index) {
|
|
if (from[index] == current) {
|
|
found = index;
|
|
break;
|
|
}
|
|
}
|
|
translated += found < to.size() ? to[found] : current;
|
|
i += current.size();
|
|
}
|
|
cycle.pattern = std::move(translated);
|
|
}
|
|
break;
|
|
case 'r': {
|
|
queueFile(cycle, command.text);
|
|
break;
|
|
}
|
|
case 'R': {
|
|
queueNextFileLine(cycle, command.text);
|
|
break;
|
|
}
|
|
case 'w':
|
|
output.writeFile(command.text, cycle.pattern, cycle.hadDelimiter);
|
|
break;
|
|
case 'W': {
|
|
const std::size_t nl = cycle.pattern.find('\n');
|
|
output.writeFile(command.text,
|
|
nl == std::string::npos ? cycle.pattern
|
|
: cycle.pattern.substr(0, nl),
|
|
true);
|
|
break;
|
|
}
|
|
case 'e':
|
|
if (command.text.empty()) {
|
|
cycle.pattern = executeShell(cycle.pattern);
|
|
cycle.hadDelimiter = true;
|
|
} else {
|
|
output.write(executeShell(command.text, false));
|
|
}
|
|
break;
|
|
case 'z':
|
|
cycle.pattern.clear();
|
|
cycle.hadDelimiter = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::string Runner::runRecords(std::vector<Record> records) {
|
|
std::ostringstream buffer;
|
|
Output output(options_, buffer);
|
|
|
|
// GNU sed allows "0r file" to emit before the first input cycle.
|
|
for (Command &command : program_.commands) {
|
|
if (command.opcode == 'r' && command.firstAddress &&
|
|
command.firstAddress->kind == AddressKind::Line &&
|
|
command.firstAddress->first == 0 && !command.secondAddress) {
|
|
Cycle prelude;
|
|
queueFile(prelude, command.text);
|
|
emitAppendQueue(prelude, output);
|
|
}
|
|
}
|
|
|
|
for (std::size_t index = 0; index < records.size(); ++index) {
|
|
const Record &record = records[index];
|
|
lastSubstitutionSucceeded_ = false;
|
|
Cycle cycle;
|
|
cycle.pattern = record.text;
|
|
cycle.hadDelimiter = record.hadDelimiter;
|
|
cycle.fileName = record.fileName;
|
|
cycle.lineNumber = record.lineNumber;
|
|
cycle.fileLineNumber = record.fileLineNumber;
|
|
cycle.lastInFile = record.lastInFile;
|
|
cycle.lastOverall = record.lastOverall;
|
|
|
|
for (Command &command : program_.commands) {
|
|
if (command.opcode == '{' && command.rangeActive &&
|
|
command.secondAddress &&
|
|
command.secondAddress->kind == AddressKind::Line) {
|
|
// Inactive braced blocks are skipped as a unit, so a block range ending
|
|
// by numeric line needs this pre-pass to close before applicability is
|
|
// tested for the current cycle.
|
|
const std::uint64_t line =
|
|
options_.separate ? cycle.fileLineNumber : cycle.lineNumber;
|
|
if (line > command.secondAddress->first) {
|
|
command.rangeActive = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (std::size_t pc = 0; pc < program_.commands.size(); ++pc) {
|
|
Command &command = program_.commands[pc];
|
|
const bool applies = commandApplies(command, cycle);
|
|
if (command.opcode == '{' && !applies) {
|
|
// Skip the entire inactive block while honoring nested braces in the
|
|
// flattened command stream.
|
|
int depth = 1;
|
|
while (pc + 1 < program_.commands.size() && depth > 0) {
|
|
++pc;
|
|
if (program_.commands[pc].opcode == '{') {
|
|
++depth;
|
|
} else if (program_.commands[pc].opcode == '}') {
|
|
--depth;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (applies) {
|
|
if (command.opcode != '{' && command.opcode != '}') {
|
|
execute(command, cycle, pc, records, index, output);
|
|
}
|
|
}
|
|
if (cycle.deleted || cycle.quit) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!cycle.deleted && !options_.quiet) {
|
|
output.writePattern(cycle.pattern, cycle.hadDelimiter);
|
|
}
|
|
emitAppendQueue(cycle, output);
|
|
if (cycle.quit) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return buffer.str();
|
|
}
|
|
|
|
int Runner::run() {
|
|
InputLoader loader(options_);
|
|
|
|
if (options_.inPlace) {
|
|
// In-place editing runs each file independently so hold space, R offsets,
|
|
// last-regex state, and line numbering match GNU sed's per-file rewrite.
|
|
if (options_.inputFiles.empty()) {
|
|
std::cerr << "sed: no input files\n";
|
|
return 4;
|
|
}
|
|
|
|
for (const std::string &fileName : options_.inputFiles) {
|
|
const std::string editName =
|
|
options_.followSymlinks ? followSymlinkEditPath(fileName) : fileName;
|
|
std::error_code ec;
|
|
const std::filesystem::file_status status =
|
|
std::filesystem::status(editName, ec);
|
|
if (ec || !std::filesystem::exists(status)) {
|
|
std::cerr << "sed: can't read " << fileName << '\n';
|
|
return 4;
|
|
}
|
|
if (std::filesystem::is_character_file(status)) {
|
|
std::cerr << "sed: couldn't edit " << fileName << ": is a terminal\n";
|
|
return 4;
|
|
}
|
|
if (!std::filesystem::is_regular_file(status)) {
|
|
std::cerr << "sed: couldn't edit " << fileName
|
|
<< ": not a regular file\n";
|
|
return 4;
|
|
}
|
|
|
|
holdSpace_.clear();
|
|
holdHadDelimiter_ = true;
|
|
lastSubstitutionSucceeded_ = false;
|
|
lastRegex_.clear();
|
|
readOffsets_.clear();
|
|
auto records = loader.loadFile(editName == "-" ? "./-" : editName);
|
|
const std::string rewritten = runRecords(std::move(records));
|
|
if (!options_.inPlaceSuffix.empty()) {
|
|
// A backup suffix containing * expands each star to the input path;
|
|
// otherwise the suffix is appended to the edited path.
|
|
std::string backup = options_.inPlaceSuffix;
|
|
std::size_t star = backup.find('*');
|
|
if (star == std::string::npos) {
|
|
backup = editName + backup;
|
|
} else {
|
|
std::string resolved;
|
|
std::size_t start = 0;
|
|
while (star != std::string::npos) {
|
|
resolved += backup.substr(start, star - start);
|
|
resolved += editName;
|
|
start = star + 1;
|
|
star = backup.find('*', start);
|
|
}
|
|
resolved += backup.substr(start);
|
|
backup = std::move(resolved);
|
|
}
|
|
if (backup != editName) {
|
|
std::error_code backupEc;
|
|
std::filesystem::copy_file(
|
|
editName, backup,
|
|
std::filesystem::copy_options::overwrite_existing, backupEc);
|
|
if (backupEc) {
|
|
std::cerr << "sed: cannot rename " << fileName << " to " << backup
|
|
<< ": " << backupEc.message() << '\n';
|
|
return 4;
|
|
}
|
|
}
|
|
}
|
|
const std::filesystem::path original(editName);
|
|
const std::filesystem::path temp =
|
|
original.parent_path() /
|
|
(original.filename().string() + ".sedtmp." +
|
|
std::to_string(::getpid()));
|
|
// Write a sibling temporary file and then rename so a failed write does
|
|
// not partially truncate the original.
|
|
std::ofstream output(temp, std::ios::binary | std::ios::trunc);
|
|
if (!output) {
|
|
std::cerr << "sed: couldn't open temporary file\n";
|
|
return 4;
|
|
}
|
|
output << rewritten;
|
|
output.close();
|
|
std::filesystem::rename(temp, original, ec);
|
|
if (ec) {
|
|
std::filesystem::remove(temp);
|
|
std::cerr << "sed: couldn't rename temporary file\n";
|
|
return 4;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const bool readsStdin =
|
|
options_.inputFiles.empty() ||
|
|
std::find(options_.inputFiles.begin(), options_.inputFiles.end(), "-") !=
|
|
options_.inputFiles.end();
|
|
if (readsStdin) {
|
|
// The upstream tests expect sed to reject nonblocking stdin rather than
|
|
// spin or return partial data.
|
|
const int flags = ::fcntl(STDIN_FILENO, F_GETFL, 0);
|
|
if (flags >= 0 && (flags & O_NONBLOCK) != 0) {
|
|
std::cerr << "sed: read error on stdin\n";
|
|
return 4;
|
|
}
|
|
}
|
|
|
|
if (options_.unbuffered && readsStdin && options_.inputFiles.empty() &&
|
|
program_.commands.size() == 1 && program_.commands.front().opcode == 'q' &&
|
|
program_.commands.front().firstAddress &&
|
|
program_.commands.front().firstAddress->kind == AddressKind::Line &&
|
|
program_.commands.front().firstAddress->first == 1) {
|
|
// A narrow streaming fast path for "sed -u '1q'": do not wait to read all
|
|
// of stdin when the program quits after the first record.
|
|
const char delimiter = options_.nullData ? '\0' : '\n';
|
|
std::string text;
|
|
if (std::getline(std::cin, text, delimiter)) {
|
|
Record record;
|
|
record.text = text;
|
|
record.fileName = "-";
|
|
record.hadDelimiter = !std::cin.eof();
|
|
record.lineNumber = 1;
|
|
record.fileLineNumber = 1;
|
|
record.lastInFile = false;
|
|
record.lastOverall = false;
|
|
std::cout << runRecords({record});
|
|
}
|
|
return finalStatus_;
|
|
}
|
|
|
|
if (options_.followSymlinks) {
|
|
// Probe symlinks before loading so readlink diagnostics happen before any
|
|
// output, matching GNU sed's command-line failure ordering.
|
|
for (const std::string &fileName : options_.inputFiles) {
|
|
if (fileName == "-") {
|
|
continue;
|
|
}
|
|
std::error_code ec;
|
|
(void)std::filesystem::symlink_status(fileName, ec);
|
|
if (ec) {
|
|
std::cerr << "sed: couldn't readlink " << fileName << ": "
|
|
<< ec.message() << '\n';
|
|
return 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<Record> records = loader.loadAll();
|
|
std::cout << runRecords(std::move(records));
|
|
return finalStatus_;
|
|
}
|
|
|
|
} // namespace sedpp
|