initial commit

This commit is contained in:
2026-06-09 00:22:22 -05:00
commit c84de3418b
25 changed files with 3501 additions and 0 deletions
+978
View File
@@ -0,0 +1,978 @@
#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