#include "sedpp/Runner.h" #include "sedpp/Output.h" #include "sedpp/Regex.h" #include "sedpp/Replacement.h" #include #include #include #include #include #include #include #include #include #include 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 splitCharacters(const std::string &text) { std::vector chars; for (std::size_t i = 0; i < text.size();) { const unsigned char ch = static_cast(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(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 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 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 groups(10, regmatch_t{-1, -1}); groups[0] = regmatch_t{static_cast(token.start), static_cast(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 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 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(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 &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(-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 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 records = loader.loadAll(); std::cout << runRecords(std::move(records)); return finalStatus_; } } // namespace sedpp