initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user