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
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <cstdint>
#include <optional>
#include <regex.h>
#include <string>
#include <vector>
namespace sedpp {
// Address kinds mirror GNU sed's address grammar. Step, Plus, and Modulo are
// GNU extensions: FIRST~STEP as a standalone address, and +N/~N only as the
// second half of a range.
enum class AddressKind {
None,
Line,
Last,
Regex,
Step,
Plus,
Modulo,
};
// A parsed address keeps both the original text and numeric forms. The text is
// still needed for special cases such as line address 0 and for empty regexes,
// which mean "reuse the previous regular expression" at runtime.
struct Address {
AddressKind kind = AddressKind::None;
std::string text;
std::uint64_t first = 0;
std::uint64_t second = 0;
};
// Parsed state for the s command. flagOrder preserves p/e ordering because GNU
// sed changes observable output depending on whether printing happens before or
// after shell execution.
struct Substitute {
std::string pattern;
std::string replacement;
bool global = false;
bool print = false;
bool execute = false;
bool ignoreCase = false;
bool multiline = false;
std::uint64_t occurrence = 0;
std::string flagOrder;
std::string writeFile;
};
// y transliteration operates on decoded character strings, not regex syntax.
struct Translate {
std::string from;
std::string to;
};
// A compiled sed command. The opcode selects which optional payload is valid:
// text for a/i/c/r/R/w/W/e, label for :/b/t/T, substitute for s, translate for y.
struct Command {
std::optional<Address> firstAddress;
std::optional<Address> secondAddress;
bool negate = false;
char opcode = '\0';
// These fields are intentionally plain. sed commands are compact and varied;
// a tiny tagged structure is easier to audit than a deep inheritance tree.
std::string text;
std::string label;
std::optional<Substitute> substitute;
std::optional<Translate> translate;
int exitCode = 0;
int lineLength = 0;
// Runtime range state. GNU sed keeps range activation per compiled command,
// not globally, so the state lives with the command object.
bool rangeActive = false;
bool rangeJustStarted = false;
std::uint64_t rangeCountdown = 0;
bool zeroRangeStarted = false;
};
// The compiler flattens the sed script into one vector. Braced blocks remain
// explicit { and } opcodes so the runner can skip inactive blocks without a
// second tree-shaped representation.
struct Program {
std::vector<Command> commands;
};
} // namespace sedpp
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include "sedpp/Command.h"
#include "sedpp/Options.h"
namespace sedpp {
// Converts raw -e/-f script text into a flat Program. The compiler is deliberately
// syntax-oriented; command range state and "last regex" behavior are resolved by
// Runner because they depend on input cycles.
class Compiler {
public:
explicit Compiler(const Options &options);
[[nodiscard]] Program compile(const std::vector<std::string> &scripts);
[[nodiscard]] const std::unordered_map<std::string, std::size_t> &labels() const {
return labels_;
}
private:
// Script parsing is a small cursor parser because sed syntax is compact and
// delimiter-driven; tokenizing first tends to lose useful byte positions.
void compileOne(const std::string &script);
[[nodiscard]] std::optional<Address> parseAddress(const std::string &script,
std::size_t &pos,
bool secondAddress);
[[nodiscard]] std::string parseDelimited(const std::string &script,
std::size_t &pos, char delimiter,
bool regexMode = false,
bool unescapeDelimiter = false);
[[nodiscard]] std::string readUntilCommandEnd(const std::string &script,
std::size_t &pos);
static void skipBlanks(const std::string &script, std::size_t &pos);
static void skipSeparators(const std::string &script, std::size_t &pos);
const Options &options_;
Program program_;
std::unordered_map<std::string, std::size_t> labels_;
};
} // namespace sedpp
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "sedpp/Options.h"
namespace sedpp {
// A loaded input record is sed's starting pattern space for one cycle. The
// delimiter bit is carried separately so files without a trailing newline remain
// distinguishable all the way to output.
struct Record {
std::string text;
std::string fileName;
bool hadDelimiter = false;
bool lastInFile = false;
bool lastOverall = false;
std::uint64_t lineNumber = 0;
std::uint64_t fileLineNumber = 0;
};
// Reads stdin or input files eagerly into records. Eager loading simplifies GNU
// sed's $ address and in-place rewrite behavior, both of which need to know the
// last record before the runner starts emitting final output.
class InputLoader {
public:
explicit InputLoader(const Options &options);
[[nodiscard]] std::vector<Record> loadAll() const;
[[nodiscard]] std::vector<Record> loadFile(const std::string &fileName) const;
private:
[[nodiscard]] std::vector<Record> splitBuffer(const std::string &buffer,
const std::string &fileName,
std::uint64_t &globalLine) const;
const Options &options_;
};
} // namespace sedpp
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
namespace sedpp {
// A script can come from -e text or -f file text. Keeping the source kind lets
// main.cpp reproduce GNU-style diagnostics before the compiler sees the script.
struct ScriptSpec {
bool isFile = false;
std::string value;
};
// Command-line options that affect both compilation and execution. The parser
// keeps compatibility switches such as posix/strictPosix separate because some
// behavior is merely POSIX-flavored while other behavior must reject GNU syntax.
struct Options {
bool quiet = false;
bool extendedRegex = false;
bool nullData = false;
bool separate = false;
bool sandbox = false;
bool unbuffered = false;
bool posix = false;
bool strictPosix = false;
bool debug = false;
bool inPlace = false;
bool followSymlinks = false;
int lineLength = 0;
std::string inPlaceSuffix;
std::vector<std::string> scripts;
std::vector<std::string> scriptFiles;
std::vector<ScriptSpec> scriptSpecs;
std::vector<std::string> inputFiles;
};
// immediateExit is set for --help/--version and for parse-time usage errors
// where sed should exit before compiling or reading input.
struct ParseResult {
Options options;
int immediateExit = -1;
};
class OptionParser {
public:
[[nodiscard]] ParseResult parse(int argc, char **argv) const;
private:
static void printHelp(bool includeEmail, bool toStdout);
static void printVersion();
};
} // namespace sedpp
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <iosfwd>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <unordered_map>
#include "sedpp/Options.h"
namespace sedpp {
// Centralizes delimiter-aware writes. Most commands emit either pattern space
// plus its original delimiter bit, or synthetic lines that always end in the
// active delimiter.
class Output {
public:
Output(const Options &options, std::ostream &stream);
void write(const std::string &text);
void writeDelimiter(bool hadDelimiter);
void writeLine(const std::string &text);
void writePattern(const std::string &pattern, bool hadDelimiter);
void writeEscaped(const std::string &pattern, bool hadDelimiter, int width);
void writeFile(const std::string &path, const std::string &text,
bool hadDelimiter);
private:
// Pending delimiters defer the trailing separator for unterminated records
// until another write proves that a separator is needed between outputs.
void writePendingDelimiter(std::ostream &target, bool &pending);
char delimiter() const;
const Options &options_;
std::ostream &stream_;
bool pendingDelimiter_ = false;
std::unordered_map<std::string, bool> pendingFileDelimiter_;
std::unordered_set<std::string> openedWriteFiles_;
};
} // namespace sedpp
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <optional>
#include <regex.h>
#include <string>
#include <vector>
#include "sedpp/Options.h"
namespace sedpp {
struct Match {
std::vector<regmatch_t> groups;
};
// Small RAII wrapper for POSIX regex_t. POSIX regex reports match offsets
// relative to the searched pointer, so search() rebases groups to the original
// subject string before returning.
class Regex {
public:
Regex(const std::string &pattern, const Options &options,
bool ignoreCase = false, bool multiline = false);
Regex(const Regex &) = delete;
Regex &operator=(const Regex &) = delete;
Regex(Regex &&other) noexcept;
Regex &operator=(Regex &&other) noexcept;
~Regex();
[[nodiscard]] std::optional<Match> search(const std::string &text,
std::size_t start = 0) const;
private:
regex_t regex_{};
bool compiled_ = false;
};
} // namespace sedpp
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <regex.h>
#include <string>
#include <vector>
namespace sedpp {
// Expands an s-command replacement. Besides &, \1...\9, and literal escapes,
// GNU sed supports case-conversion state such as \U...\E and one-byte \u/\l.
class Replacement {
public:
static std::string expand(const std::string &replacement,
const std::string &subject,
const std::vector<regmatch_t> &groups);
};
} // namespace sedpp
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "sedpp/Command.h"
#include "sedpp/Input.h"
#include "sedpp/Options.h"
namespace sedpp {
// Executes a compiled sed program against the loaded input records. This class
// owns the mutable pieces of sed state: hold space, last successful substitution,
// last regex, per-command ranges, and read offsets for R commands.
class Runner {
public:
Runner(Options options, Program program,
std::unordered_map<std::string, std::size_t> labels);
[[nodiscard]] int run();
private:
// One sed cycle starts with a single input record in pattern space. Commands
// may append more input (N/n), delete the cycle, or request a quit before the
// automatic print phase.
struct Cycle {
std::string pattern;
bool hadDelimiter = false;
std::string fileName;
std::uint64_t lineNumber = 0;
std::uint64_t fileLineNumber = 0;
bool lastInFile = false;
bool lastOverall = false;
bool deleted = false;
bool quit = false;
int exitCode = 0;
std::vector<std::string> appendQueue;
};
// runRecords is shared by normal output and in-place editing so in-place mode
// can render to memory before atomically replacing the source file.
[[nodiscard]] std::string runRecords(std::vector<Record> records);
[[nodiscard]] bool commandApplies(Command &command, const Cycle &cycle);
[[nodiscard]] bool addressMatches(const Address &address, const Cycle &cycle);
void execute(Command &command, Cycle &cycle, std::size_t &pc,
std::vector<Record> &records, std::size_t &recordIndex,
class Output &output);
bool substitute(Command &command, Cycle &cycle, class Output &output);
std::string executeShell(const std::string &command,
bool stripTrailingDelimiter = true) const;
void emitAppendQueue(Cycle &cycle, class Output &output);
void queueFile(Cycle &cycle, const std::string &path);
void queueNextFileLine(Cycle &cycle, const std::string &path);
Options options_;
Program program_;
std::unordered_map<std::string, std::size_t> labels_;
// Branch commands t/T observe whether any s command has succeeded since the
// last input-cycle start or branch test.
bool lastSubstitutionSucceeded_ = false;
// Empty regex addresses and empty s patterns reuse this value, matching sed's
// "last regular expression" rule.
std::string lastRegex_;
// Hold space has its own delimiter bit because pattern/hold exchanges must
// preserve whether the original input ended with a record separator.
std::string holdSpace_;
bool holdHadDelimiter_ = true;
// R reads one line per invocation per path; offsets remember the next line.
std::unordered_map<std::string, std::size_t> readOffsets_;
int finalStatus_ = 0;
};
} // namespace sedpp
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <string>
namespace sedpp {
// Minimal POSIX-style path helpers used by command-line and file-output code.
// They intentionally avoid filesystem canonicalization so sed path strings stay
// close to what the user supplied.
[[nodiscard]] bool isAbsolutePath(const std::string &path);
[[nodiscard]] std::string basenameOf(const std::string &path);
[[nodiscard]] std::string joinPath(const std::string &directory,
const std::string &name);
} // namespace sedpp