mirror of
https://github.com/uutils/red.git
synced 2026-06-10 16:14:42 -07:00
778 lines
24 KiB
Markdown
778 lines
24 KiB
Markdown
# Red Architecture
|
||
|
||
**Rust implementation of GNU sed - Stream Editor**
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Overview](#overview)
|
||
2. [Core Architecture](#core-architecture)
|
||
3. [Module Structure](#module-structure)
|
||
4. [Key Components](#key-components)
|
||
5. [Data Flow](#data-flow)
|
||
6. [Bytes and Encoding](#bytes-and-encoding)
|
||
7. [Regex Engine](#regex-engine)
|
||
8. [GNU sed Compatibility](#gnu-sed-compatibility)
|
||
9. [Development Guide](#development-guide)
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
Red is a drop-in replacement for GNU sed, written in Rust.
|
||
|
||
### Design Principles
|
||
|
||
1. **Two-Phase Processing**: Compile scripts once, execute many times
|
||
2. **Unified Context**: Single configuration object passed through all modules
|
||
3. **Type Safety**: Leverage Rust's type system for correctness
|
||
4. **GNU Compatibility**: Match GNU sed behavior
|
||
|
||
---
|
||
|
||
## Core Architecture
|
||
|
||
### Comparison with GNU sed
|
||
|
||
| Aspect | GNU sed | Red | Notes |
|
||
|--------|---------|-----|-------|
|
||
| Command representation | 1 struct with union | 2 enums (parser + runtime) | Standard AST→IR pattern |
|
||
| Processing | Byte-first | String-first (UTF-8) | Raw bytes tracked separately |
|
||
| Regex engine | POSIX regex + DFA | Custom 3-level matcher | Literal/DFA/NFA |
|
||
| Memory | Obstack allocator | Standard Rust allocations | |
|
||
| Jumps | Array indices | Pre-resolved indices | Resolved at compile time |
|
||
|
||
### High-Level Flow
|
||
|
||
```
|
||
┌─────────────┐
|
||
│ CLI Args │
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
┌─────────────┐ ┌──────────────┐
|
||
│ Context │────▶│ Validation │
|
||
└──────┬──────┘ └──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ Parser │────▶│ Lexer │────▶│ AST Nodes │
|
||
└──────┬──────┘ └──────────────┘ └──────────────┘
|
||
│ (parser::Command)
|
||
│
|
||
▼
|
||
┌────────────────────┐
|
||
│ convert_to_runtime │ (AST → RuntimeCommand)
|
||
└──────┬─────────────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Commands │ (RuntimeCommand with compiled regexes)
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ Engine │────▶│ Evaluator │────▶│ Output │
|
||
└─────────────┘ └──────────────┘ └──────────────┘
|
||
```
|
||
|
||
### Phase 1: Compilation
|
||
|
||
**Input**: Script text + Context
|
||
**Output**: List of `RuntimeCommand`
|
||
**Purpose**: Validate and optimize scripts before execution
|
||
|
||
```rust
|
||
pub fn parse_scripts_to_commands(
|
||
scripts: &[(String, ScriptSource)],
|
||
ctx: &Context,
|
||
) -> Result<Vec<RuntimeCommand>>
|
||
```
|
||
|
||
### Phase 2: Execution
|
||
|
||
**Input**: Commands + Input lines
|
||
**Output**: Transformed text
|
||
**Purpose**: Apply commands to input data
|
||
|
||
```rust
|
||
pub fn execute_over_lines(
|
||
commands: &[RuntimeCommand],
|
||
// ... parameters
|
||
) -> Result<()>
|
||
```
|
||
|
||
---
|
||
|
||
## Module Structure
|
||
|
||
### Module Dependency Graph
|
||
|
||
```
|
||
main.rs
|
||
│
|
||
▼
|
||
cli.rs ──────────────────────────────────────┐
|
||
│ │
|
||
▼ ▼
|
||
lib.rs ◄──────────────────────────────── errors.rs
|
||
│
|
||
├──────────────┬──────────────┬──────────────┐
|
||
▼ ▼ ▼ ▼
|
||
parser/ engine/ fileio/ util/
|
||
├── mod.rs ├── mod.rs ├── mod.rs ├── regex.rs
|
||
├── lexer.rs ├── exec.rs ├── lines.rs └── version.rs
|
||
└── ast.rs ├── addr.rs ├── inplace.rs
|
||
├── types.rs └── encoding.rs
|
||
└── pattern_space.rs
|
||
│
|
||
▼
|
||
regex/
|
||
├── mod.rs
|
||
├── parser.rs
|
||
├── dfa.rs
|
||
├── nfa.rs
|
||
├── backtrack.rs
|
||
└── literal.rs
|
||
```
|
||
|
||
### 1. CLI Layer (`src/cli.rs`, `src/main.rs`)
|
||
|
||
**Responsibility**: Argument parsing and validation
|
||
|
||
- Uses `lexopt` for natural order preservation
|
||
- Separate `print_usage()` and `help_text()` functions
|
||
- Exit codes match GNU sed (4 for usage errors)
|
||
|
||
**Key Functions**:
|
||
```rust
|
||
pub fn parse_args() -> Result<CliArgs>
|
||
```
|
||
|
||
### 2. Context Layer (`src/context.rs`)
|
||
|
||
**Responsibility**: Unified configuration management
|
||
|
||
```rust
|
||
pub struct Context {
|
||
// Regex mode
|
||
pub extended_regex: bool,
|
||
|
||
// POSIX compliance
|
||
pub posix: PosixMode,
|
||
pub strict_posix: bool,
|
||
|
||
// Execution modes
|
||
pub quiet: bool,
|
||
pub null_data: bool,
|
||
pub unbuffered: bool,
|
||
pub separate_files: bool,
|
||
|
||
// In-place editing
|
||
pub in_place_suffix: Option<String>,
|
||
pub follow_symlinks: bool,
|
||
|
||
// Formatting
|
||
pub line_length: usize,
|
||
|
||
// Security
|
||
pub sandbox: bool,
|
||
|
||
// Scripts (for #n quiet mode detection)
|
||
pub scripts_with_sources: Vec<(String, ScriptSource)>,
|
||
}
|
||
```
|
||
|
||
**POSIX Modes**:
|
||
```rust
|
||
pub enum PosixMode {
|
||
Extended, // GNU extensions enabled (default)
|
||
Correct, // POSIXLY_CORRECT env var
|
||
Basic, // --posix flag (strict)
|
||
}
|
||
```
|
||
|
||
### 3. File I/O Layer (`src/fileio/`)
|
||
|
||
**Responsibility**: File reading, encoding detection, and backup utilities
|
||
|
||
- `encoding.rs` - Encoding detection from BOM and locale
|
||
- `lines.rs` - File/stdin reading with line splitting
|
||
- `inplace.rs` - Backup suffix expansion for in-place editing
|
||
- `mod.rs` - Module exports and output flushing helper
|
||
|
||
**Key Functions**:
|
||
```rust
|
||
pub fn read_all_lines(files, null_data, follow_symlinks) -> Result<(lines, bytes, filenames, encoding, ends_with_sep)>
|
||
pub fn split_file_content(content, null_data) -> (lines, bytes, encoding, ends_with_sep)
|
||
pub fn expand_backup_suffix(file_path, suffix) -> String
|
||
pub fn flush_output(out, unbuffered)
|
||
```
|
||
|
||
### 4. Parser Layer (`src/parser/`)
|
||
|
||
**Responsibility**: Script compilation and validation
|
||
|
||
#### Lexer (`src/parser/lexer.rs`)
|
||
|
||
Tokenizes sed scripts using a state machine approach.
|
||
|
||
**Key optimizations**:
|
||
- Single-pass tokenization
|
||
- Bracket depth tracking
|
||
- Escape sequence handling
|
||
- Raw bytes preservation for non-UTF-8
|
||
|
||
#### AST (`src/parser/ast.rs`)
|
||
|
||
Defines parser command structure:
|
||
|
||
```rust
|
||
pub enum Command {
|
||
Substitute { flags, pattern, replacement, ... },
|
||
Delete { range, negated },
|
||
Print { range, negated, ... },
|
||
Group { commands }, // Flattened at compile time
|
||
Comment(String), // Discarded at compile time
|
||
Version { version }, // Checked at compile time
|
||
// ... more command types
|
||
}
|
||
```
|
||
|
||
#### Parser (`src/parser.rs`)
|
||
|
||
Converts tokens to AST nodes with validation:
|
||
|
||
```rust
|
||
pub fn parse(tokens: &[Token], ctx: &Context) -> Result<Vec<Command>>
|
||
```
|
||
|
||
### 5. Engine Layer (`src/engine/`)
|
||
|
||
**Responsibility**: Command execution and state management
|
||
|
||
#### Types (`src/engine/types.rs`)
|
||
|
||
Runtime command representation:
|
||
|
||
```rust
|
||
pub enum Command {
|
||
Substitution {
|
||
pattern: SedRegex, // Compiled regex
|
||
replacement: ReplacementTemplate, // Parsed replacement
|
||
global: bool,
|
||
print: bool,
|
||
literal_pattern: Option<String>, // Fast-path optimization
|
||
literal_replacement: Option<String>,
|
||
// ... more fields
|
||
},
|
||
// ... more runtime commands
|
||
}
|
||
```
|
||
|
||
#### ExecutionContext (`src/engine/exec.rs`)
|
||
|
||
Maintains execution state:
|
||
|
||
```rust
|
||
pub struct ExecutionContext {
|
||
pub pattern_space: PatternSpace,
|
||
pub hold_space: String,
|
||
pub hold_space_raw: Vec<u8>,
|
||
pub line_number: usize,
|
||
pub is_last_line: bool,
|
||
// ... state fields
|
||
}
|
||
```
|
||
|
||
#### AddressEvaluator (`src/engine/addr.rs`)
|
||
|
||
Evaluates address ranges with state tracking:
|
||
|
||
```rust
|
||
pub fn evaluate(&mut self, range: Option<&AddressRange>, ctx: &ExecutionContext) -> bool
|
||
```
|
||
|
||
**Features**:
|
||
- Line number ranges (5,10)
|
||
- Regex matches (/pattern/)
|
||
- Step addressing (1~2)
|
||
- Dollar ($) for last line
|
||
- Range state machine (INACTIVE → ACTIVE → CLOSED)
|
||
|
||
### 6. Error Handling (`src/errors.rs`)
|
||
|
||
**Unified error types**:
|
||
|
||
```rust
|
||
pub enum SedError {
|
||
Parse { message, source },
|
||
Io { operation, path, error },
|
||
Rename { source_path, dest_path, error },
|
||
Runtime { message },
|
||
Usage { message },
|
||
}
|
||
```
|
||
|
||
**GNU sed compatible error messages and exit codes**.
|
||
|
||
---
|
||
|
||
## Key Components
|
||
|
||
### Two-Stage Command Representation
|
||
|
||
Red uses a deliberate two-stage command representation, following the classic AST-to-IR pattern from compilers:
|
||
|
||
#### Parser Command (`parser/ast.rs`)
|
||
|
||
```rust
|
||
pub enum Command {
|
||
Substitution {
|
||
pattern: String, // Raw regex pattern
|
||
replacement: String, // Raw replacement string
|
||
flags: SubstitutionFlags, // Struct with all flags
|
||
// ...
|
||
},
|
||
Group { commands: Vec<Command> }, // Flattened at compile time
|
||
Version { version: String }, // Compile-time check only
|
||
Comment(String), // Discarded
|
||
// ... more variants
|
||
}
|
||
```
|
||
|
||
**Purpose**: Preserves source structure, enables error reporting with context
|
||
|
||
#### Runtime Command (`engine/types.rs`)
|
||
|
||
```rust
|
||
pub enum Command {
|
||
Substitution {
|
||
pattern: SedRegex, // Compiled regex
|
||
replacement: ReplacementTemplate, // Parsed replacement
|
||
global: bool, // Expanded from flags
|
||
print: bool,
|
||
literal_pattern: Option<String>, // Fast-path optimization
|
||
literal_replacement: Option<String>,
|
||
// ... fields with optimizations
|
||
},
|
||
// ... more variants (no Group/Version/Comment)
|
||
}
|
||
```
|
||
|
||
**Purpose**: Optimized for execution, pre-compiled patterns, literal optimizations
|
||
|
||
#### Why Two Enums?
|
||
|
||
1. **Separation of concerns**: Parser produces semantic representation, engine uses optimized representation
|
||
2. **Compile-time processing**: Group commands are flattened, Version is checked, Comments discarded
|
||
3. **Optimization fields**: Runtime adds `literal_pattern`, `literal_replacement`, `use_last` that don't exist in source
|
||
4. **Type differences**: String → SedRegex, String → ReplacementTemplate, SubstitutionFlags → individual booleans
|
||
5. **Common compiler pattern**: Similar to AST → IR transformation in production compilers
|
||
|
||
### PatternSpace: Dual Representation
|
||
|
||
The `PatternSpace` struct (`src/engine/pattern_space.rs`) maintains synchronized representations:
|
||
|
||
```rust
|
||
pub struct PatternSpace {
|
||
raw: Vec<u8>, // Raw bytes (may contain invalid UTF-8)
|
||
active_start: usize, // Active pointer for D command optimization
|
||
text_cache: Option<String>, // Cached UTF-8 representation
|
||
}
|
||
```
|
||
|
||
**Key methods**:
|
||
- `raw()` - Returns active bytes slice
|
||
- `text()` - Returns UTF-8 text (from cache)
|
||
- `set_raw()` - Updates from bytes, rebuilds text cache with lossy conversion
|
||
- `delete_first_line()` - O(1) using active pointer (GNU sed technique)
|
||
|
||
---
|
||
|
||
## Data Flow
|
||
|
||
### 1. Script Compilation
|
||
|
||
```
|
||
Script Text
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Lexer │ Tokenize with raw bytes tracking
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Parser │ Build AST (parser::Command)
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Validator │ Check POSIX rules, labels
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
┌─────────────┐
|
||
│ Converter │ compile_to_runtime_commands()
|
||
└──────┬──────┘
|
||
│
|
||
▼
|
||
RuntimeCommand[] (with compiled regexes)
|
||
```
|
||
|
||
### 2. Command Execution
|
||
|
||
```
|
||
Input Lines (with raw bytes)
|
||
│
|
||
▼
|
||
┌──────────────────┐
|
||
│ ExecutionContext │◄─── Commands
|
||
│ - pattern_space │
|
||
│ - hold_space │
|
||
│ - line_number │
|
||
└────────┬─────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────┐
|
||
│ apply_commands_with_ │
|
||
│ context() │
|
||
└───────┬──────────────┘
|
||
│
|
||
▼
|
||
CommandResult (with raw bytes)
|
||
│
|
||
▼
|
||
┌────────┐
|
||
│ Output │ (preserves raw bytes)
|
||
└────────┘
|
||
```
|
||
|
||
### 3. In-Place Editing
|
||
|
||
```
|
||
Original File
|
||
│
|
||
▼
|
||
┌──────────────┐
|
||
│ Read Content │ (as raw bytes)
|
||
└──────┬───────┘
|
||
│
|
||
▼
|
||
┌──────────────┐
|
||
│ Process │ (preserve raw bytes through pipeline)
|
||
└──────┬───────┘
|
||
│
|
||
▼
|
||
┌──────────────┐
|
||
│ Temp File │
|
||
└──────┬───────┘
|
||
│
|
||
▼
|
||
┌──────────────┐
|
||
│ Rename │ (atomic)
|
||
└──────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Bytes and Encoding
|
||
|
||
### Design: UTF-8 First with Raw Bytes Tracking
|
||
|
||
Red uses Rust's `String` type (valid UTF-8) as the primary text representation for regex matching, but maintains raw bytes throughout the pipeline for output.
|
||
|
||
### Raw Bytes Preservation
|
||
|
||
```
|
||
Input (bytes) → PatternSpace.raw → Commands → Output (bytes)
|
||
↓
|
||
text_cache (lossy)
|
||
↓
|
||
Regex matching on text
|
||
```
|
||
|
||
**Implementation**:
|
||
- Hold space raw bytes tracking (`hold_space_raw: Vec<u8>`)
|
||
- `CommandResult::Continue(String, Option<Vec<u8>>)` - carries raw bytes through engine
|
||
- `CommandResult::Print(String, Option<Vec<u8>>)` - outputs raw bytes when available
|
||
- `LiteralBytes(Vec<u8>)` token in replacement templates
|
||
|
||
### Component Status
|
||
|
||
| Component | Status | Implementation |
|
||
|-----------|--------|----------------|
|
||
| Script reading | Bytes | `Vec<u8>` + lossy UTF-8 conversion |
|
||
| Lexer | Bytes-aware | char-to-byte mapping for raw bytes |
|
||
| Parser | Bytes-aware | `replacement_raw_bytes` field |
|
||
| Replacement template | Bytes | `LiteralBytes(Vec<u8>)` token |
|
||
| Escape sequences | Raw output | `\xNN`, `\oNNN`, `\dNNN`, `\cX` |
|
||
| Pattern matching | UTF-8 | Regex operates on text cache |
|
||
| Data I/O | Bytes | Raw bytes preserved through hold/pattern space |
|
||
| Substitution pipeline | Bytes | Raw bytes tracked across multiple substitutions |
|
||
|
||
---
|
||
|
||
## Regex Engine
|
||
|
||
Red implements a custom regex engine with three different matchers, each optimized for different pattern types.
|
||
|
||
### Three-Level Optimization Strategy
|
||
|
||
```rust
|
||
pub enum Matcher {
|
||
Literal(LiteralMatcher), // Fastest: direct string/byte search
|
||
Dfa(DfaMatcher), // Fast: Deterministic Finite Automaton
|
||
Nfa(NfaMatcher), // Full-featured: Nondeterministic Finite Automaton
|
||
}
|
||
```
|
||
|
||
### Matcher Selection (compile time)
|
||
|
||
```
|
||
Pattern Analysis
|
||
│
|
||
▼
|
||
┌─────────────────────┐
|
||
│ Is literal? │──Yes──▶ LiteralMatcher (fastest)
|
||
│ (no metacharacters) │
|
||
└────────┬────────────┘
|
||
│ No
|
||
▼
|
||
┌─────────────────┐
|
||
│ Has backrefs? │──Yes──▶ NfaMatcher (required)
|
||
│ (\1, \2, etc) │
|
||
└────────┬────────┘
|
||
│ No
|
||
▼
|
||
┌─────────────────┐
|
||
│ MBCS locale? │──Yes──▶ NfaMatcher (for non-ASCII patterns)
|
||
│ (MB_CUR_MAX > 1)│
|
||
└────────┬────────┘
|
||
│ No
|
||
▼
|
||
DfaMatcher (fast, no backrefs)
|
||
```
|
||
|
||
### Literal Matcher (`src/regex/literal.rs`)
|
||
|
||
**When used**: Patterns without any regex metacharacters (e.g., `hello`, `foo_bar`)
|
||
|
||
**Implementation**:
|
||
```rust
|
||
pub struct LiteralMatcher {
|
||
pattern: String,
|
||
ignore_case: bool,
|
||
}
|
||
|
||
impl LiteralMatcher {
|
||
// String-based matching (UTF-8)
|
||
pub fn find(&self, text: &str) -> Option<usize>
|
||
|
||
// Byte-based matching (MBCS-safe for ASCII patterns)
|
||
pub fn find_bytes(&self, bytes: &[u8]) -> Option<usize>
|
||
pub fn find_bytes_from(&self, bytes: &[u8], start: usize) -> Option<usize>
|
||
}
|
||
```
|
||
|
||
**MBCS behavior**: ASCII-only literal patterns can safely use byte-level matching even in MBCS locales, because ASCII bytes (0x00-0x7F) never appear as part of multibyte characters.
|
||
|
||
### DFA Matcher (`src/regex/dfa.rs`)
|
||
|
||
**When used**: Simple patterns without backreferences in single-byte locales
|
||
|
||
**How DFA works**:
|
||
- Compiles pattern into a state machine at parse time
|
||
- Each state has exactly one transition per input character
|
||
- No backtracking - processes input in a single pass
|
||
- Memory: O(states × alphabet_size)
|
||
|
||
**Limitations**:
|
||
- **Cannot support backreferences** - This is a mathematical impossibility, not an implementation limitation. Backreferences (`\1`, `\2`) require the regex engine to "remember" what was matched and compare against it later. DFAs have no memory of the path taken to reach a state.
|
||
- **Disabled in MBCS locales** - DFA operates on fixed-width input units. In MBCS, character widths vary (1-4 bytes), requiring boundary detection that DFA architecture doesn't support.
|
||
|
||
### NFA Matcher (`src/regex/nfa.rs`, `src/regex/backtrack.rs`)
|
||
|
||
**When used**:
|
||
- Patterns with backreferences (`\1`, `\2`, etc.)
|
||
- Patterns with capturing groups that need extraction
|
||
- All patterns in MBCS locales (for non-ASCII patterns)
|
||
|
||
**How NFA differs from DFA**:
|
||
```
|
||
DFA NFA
|
||
┌─────────────────┐ ┌─────────────────┐
|
||
Input 'a' → │ State 1 ──────▶ │ State 2 │ State 1 ──┬───▶ │ State 2
|
||
│ (one path) │ │ └───▶ │ State 3
|
||
└─────────────────┘ │ (multiple) │
|
||
└─────────────────┘
|
||
```
|
||
|
||
- NFA can be in multiple states simultaneously
|
||
- Explores all possible paths (via backtracking)
|
||
- Can "remember" captured groups for backreferences
|
||
- More flexible but slower
|
||
|
||
**MBCS support in NFA**:
|
||
```rust
|
||
// NFA uses MbText for character boundary tracking
|
||
pub struct MbText<'a> {
|
||
raw: &'a [u8],
|
||
char_boundaries: Vec<usize>, // Where each character starts
|
||
char_validity: Vec<bool>, // Is each sequence valid?
|
||
}
|
||
|
||
// Transitions respect character boundaries
|
||
match transition {
|
||
Transition::Any => {
|
||
// In MBCS mode: advance by full character, not byte
|
||
let char_len = mbtext.char_at(pos).len();
|
||
next_pos = pos + char_len;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Why Both DFA and NFA?
|
||
|
||
| Aspect | DFA | NFA |
|
||
|--------|-----|-----|
|
||
| Speed | O(n) - very fast | O(n×m) - slower |
|
||
| Backreferences | Impossible | Full support |
|
||
| Memory usage | Higher (state table) | Lower |
|
||
| MBCS support | Disabled | Full support |
|
||
| Use case | Simple patterns | Complex patterns |
|
||
|
||
**Cannot remove either**:
|
||
- Removing DFA: Performance regression for simple patterns
|
||
- Removing NFA: Backreferences would be impossible (`s/\(foo\)/\1\1/` fails)
|
||
|
||
### MBCS Locale Behavior
|
||
|
||
When `MB_CUR_MAX > 1` (Shift-JIS, EUC-JP, etc.):
|
||
|
||
```
|
||
Pattern Type Matcher Used Why
|
||
─────────────────────────────────────────────────────
|
||
ASCII literal "hello" LiteralMatcher Safe: ASCII bytes don't overlap MBCS
|
||
Non-ASCII literal "日本" NfaMatcher Needs MBCS boundary handling
|
||
Any with backrefs NfaMatcher DFA can't do backrefs
|
||
Simple /[a-z]+/ NfaMatcher DFA disabled in MBCS
|
||
```
|
||
|
||
**Example of why DFA fails in MBCS**:
|
||
|
||
```
|
||
Shift-JIS "表" = bytes [0x95, 0x5C]
|
||
ASCII "\" = byte [0x5C]
|
||
|
||
Pattern: /\\/ (match backslash)
|
||
|
||
DFA (byte-level): Would incorrectly match inside "表"
|
||
because 0x5C appears as second byte
|
||
|
||
NFA (MBCS-aware): Correctly skips "表" because MbText
|
||
knows 0x5C is part of a 2-byte character
|
||
```
|
||
|
||
---
|
||
|
||
## GNU sed Compatibility
|
||
|
||
### Compatibility Features
|
||
|
||
#### CLI Compatibility
|
||
- Natural order preservation of `-e` and `-f`
|
||
- Separate usage (stderr) vs help (stdout) messages
|
||
- Exit code 4 for usage errors
|
||
- Write error detection (`/dev/full`)
|
||
|
||
#### Command Compatibility
|
||
- All standard sed commands (s, d, p, a, i, c, etc.)
|
||
- Address ranges (1,10, /start/,/end/, $)
|
||
- Step addressing (1~2)
|
||
- Hold space operations (h, H, g, G, x)
|
||
- Branching (b, t, T, :label)
|
||
- Byte-level operations (l command with octal escapes)
|
||
|
||
#### Multibyte Character Set (MBCS) Support
|
||
|
||
Full MBCS support for non-UTF-8 locales (Shift-JIS, EUC-JP, etc.)
|
||
|
||
| Feature | Implementation |
|
||
|---------|----------------|
|
||
| Locale detection | `mbcs::initialize()` via `setlocale()`, `MB_CUR_MAX` |
|
||
| Character boundaries | `MbText` struct with `mbrlen()` |
|
||
| Regex dot matching | `Transition::Any` respects MB boundaries |
|
||
| Character classes | `mbrtowc()` conversion for CharSet matching |
|
||
| Capture groups | `raw_bytes` field preserves MBCS bytes |
|
||
| Backreferences | Byte-level comparison in MB mode |
|
||
| Invalid/incomplete sequences | `char_validity` tracking, skip in dot match |
|
||
| Raw bytes output | `render_replacement_to_bytes()` sink |
|
||
|
||
**Key MBCS Data Structures**:
|
||
```rust
|
||
pub struct MbText<'a> {
|
||
raw: &'a [u8], // Raw input bytes
|
||
char_boundaries: Vec<usize>, // Byte offset for each char start
|
||
char_validity: Vec<bool>, // Valid vs invalid/incomplete
|
||
}
|
||
|
||
pub struct MbChar<'a> {
|
||
bytes: &'a [u8], // One logical character's bytes
|
||
}
|
||
```
|
||
|
||
#### SELinux Support
|
||
|
||
When built with the `selinux` feature (Linux only):
|
||
- Preserves SELinux contexts during in-place editing
|
||
- Without `--follow-symlinks`: Uses symlink's context for new file
|
||
- With `--follow-symlinks`: Uses target file's context
|
||
|
||
---
|
||
|
||
## Development Guide
|
||
|
||
### Adding New Commands
|
||
|
||
1. Add token in `src/parser/lexer.rs`
|
||
2. Add AST node in `src/parser/ast.rs`
|
||
3. Add parsing logic in `src/parser.rs`
|
||
4. Add runtime command in `src/engine/types.rs`
|
||
5. Add conversion in `src/lib.rs:convert_new_command_to_old()`
|
||
6. Add execution logic in `src/engine/exec.rs`
|
||
7. Add tests in `tests/`
|
||
|
||
### Adding New Options
|
||
|
||
1. Add field to `CliArgs` in `src/cli.rs`
|
||
2. Add parsing in `parse_args()`
|
||
3. Add to `Context` in `src/context.rs`
|
||
4. Update `RunConfig` in `src/lib.rs`
|
||
5. Add validation if needed
|
||
|
||
### Testing
|
||
|
||
```bash
|
||
# Unit tests
|
||
cargo test
|
||
|
||
# GNU sed compatibility tests
|
||
./scripts/run_gnused_tests.sh
|
||
|
||
# Specific test
|
||
./scripts/run_gnused_tests.sh --test-pattern "help.sh"
|
||
|
||
# Fast mode (debug build, compact output)
|
||
./scripts/run_gnused_tests.sh --fast
|
||
```
|
||
|
||
---
|
||
|
||
## References
|
||
|
||
- [GNU sed Manual](https://www.gnu.org/software/sed/manual/)
|
||
- [POSIX sed Specification](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html)
|