Skip to main content

sed_rs/
lib.rs

1//! # sed-rs
2//!
3//! A GNU-compatible `sed` (stream editor) implementation in Rust.
4//!
5//! This crate can be used both as a standalone command-line tool and as a
6//! library for programmatic stream editing.
7//!
8//! ## Quick start
9//!
10//! ```rust
11//! // Simple substitution
12//! let output = sed_rs::eval("s/hello/world/", "hello there\n").unwrap();
13//! assert_eq!(output, "world there\n");
14//!
15//! // Global substitution
16//! let output = sed_rs::eval("s/o/0/g", "foo boo\n").unwrap();
17//! assert_eq!(output, "f00 b00\n");
18//!
19//! // Delete lines matching a pattern
20//! let output = sed_rs::eval("/^#/d", "# comment\ncode\n").unwrap();
21//! assert_eq!(output, "code\n");
22//!
23//! // Multiple commands
24//! let output = sed_rs::eval("s/a/X/; s/b/Y/", "ab\n").unwrap();
25//! assert_eq!(output, "XY\n");
26//! ```
27//!
28//! ## Advanced usage
29//!
30//! For more control, use [`Sed`] directly:
31//!
32//! ```rust
33//! use sed_rs::Sed;
34//!
35//! let mut sed = Sed::new("s/foo/bar/g").unwrap();
36//! sed.quiet(true);           // suppress auto-print (-n)
37//!
38//! let output = sed.eval("no match here\n").unwrap();
39//! assert_eq!(output, "");    // quiet mode: nothing printed unless explicit `p`
40//! ```
41//!
42//! ## Using the lower-level API
43//!
44//! The [`command`] and [`engine`] modules expose the parser and execution
45//! engine for full control:
46//!
47//! ```rust
48//! use sed_rs::{command, engine, Options};
49//!
50//! let commands = command::parse("2d").unwrap();
51//! let options = Options::default();
52//! let engine = engine::Engine::new(commands, &options).unwrap();
53//! // engine.run(&[]) reads from stdin, engine.run(&[path]) reads files
54//! ```
55
56pub mod cli;
57pub mod command;
58pub mod engine;
59pub mod error;
60pub mod unescape;
61
62pub use cli::Options;
63pub use error::{Error, Result};
64
65use std::io;
66
67// ---------------------------------------------------------------------------
68// Convenience API
69// ---------------------------------------------------------------------------
70
71/// A configured sed instance that can process text.
72///
73/// This is the recommended entry point for library usage. It wraps the
74/// lower-level [`command::parse`] and [`engine::Engine`] with a builder-style
75/// API.
76///
77/// # Examples
78///
79/// ```rust
80/// use sed_rs::Sed;
81///
82/// let output = Sed::new("s/hello/world/")
83///     .unwrap()
84///     .eval("hello\n")
85///     .unwrap();
86/// assert_eq!(output, "world\n");
87/// ```
88pub struct Sed {
89    options: Options,
90    script: String,
91}
92
93impl Sed {
94    /// Create a new `Sed` instance from a sed script string.
95    ///
96    /// The script is validated (parsed and regex-compiled) eagerly; an
97    /// error is returned immediately if the script is malformed.
98    pub fn new(script: &str) -> Result<Self> {
99        // Validate the script eagerly
100        let cmds = command::parse(script)?;
101        let opts = Options::default();
102        let _ = engine::Engine::new(cmds, &opts)?;
103
104        Ok(Self {
105            options: opts,
106            script: script.to_string(),
107        })
108    }
109
110    /// Suppress automatic printing of the pattern space (equivalent to
111    /// the `-n` / `--quiet` flag).
112    pub fn quiet(&mut self, yes: bool) -> &mut Self {
113        self.options.quiet = yes;
114        self
115    }
116
117    /// Use NUL (`\0`) as the line delimiter instead of newline
118    /// (equivalent to `-z` / `--null-data`).
119    pub fn null_data(&mut self, yes: bool) -> &mut Self {
120        self.options.null_data = yes;
121        self
122    }
123
124    /// Evaluate the script against the given input string and return
125    /// the output as a `String`.
126    pub fn eval(&self, input: &str) -> Result<String> {
127        self.eval_bytes(input.as_bytes())
128    }
129
130    /// Evaluate the script against raw bytes and return the output as
131    /// a `String`.
132    pub fn eval_bytes(&self, input: &[u8]) -> Result<String> {
133        let commands = command::parse(&self.script)?;
134        let engine = engine::Engine::new(commands, &self.options)?;
135        let reader = io::BufReader::new(io::Cursor::new(input));
136        let mut output = Vec::new();
137        engine.process_stream(reader, &mut output)?;
138        Ok(String::from_utf8_lossy(&output).into_owned())
139    }
140
141    /// Evaluate the script by reading from a [`std::io::Read`] source
142    /// and writing to a [`std::io::Write`] sink.
143    pub fn eval_stream<R: io::Read, W: io::Write>(
144        &self,
145        reader: R,
146        writer: &mut W,
147    ) -> Result<()> {
148        let commands = command::parse(&self.script)?;
149        let engine = engine::Engine::new(commands, &self.options)?;
150        let buf_reader = io::BufReader::new(reader);
151        engine.process_stream(buf_reader, writer)
152    }
153}
154
155/// Evaluate a sed script against an input string and return the result.
156///
157/// This is the simplest way to use the library. For repeated use with
158/// the same script, prefer [`Sed::new`] to avoid re-parsing.
159///
160/// # Examples
161///
162/// ```rust
163/// let output = sed_rs::eval("s/world/rust/", "hello world\n").unwrap();
164/// assert_eq!(output, "hello rust\n");
165/// ```
166pub fn eval(script: &str, input: &str) -> Result<String> {
167    Sed::new(script)?.eval(input)
168}
169
170// ---------------------------------------------------------------------------
171// Tests
172// ---------------------------------------------------------------------------
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn eval_basic() {
180        assert_eq!(eval("s/foo/bar/", "foo\n").unwrap(), "bar\n");
181    }
182
183    #[test]
184    fn eval_global() {
185        assert_eq!(eval("s/o/0/g", "foo\n").unwrap(), "f00\n");
186    }
187
188    #[test]
189    fn eval_delete() {
190        assert_eq!(eval("2d", "a\nb\nc\n").unwrap(), "a\nc\n");
191    }
192
193    #[test]
194    fn eval_multiple_commands() {
195        assert_eq!(eval("s/a/X/; s/b/Y/", "ab\n").unwrap(), "XY\n");
196    }
197
198    #[test]
199    fn eval_empty_input() {
200        assert_eq!(eval("s/a/b/", "").unwrap(), "");
201    }
202
203    #[test]
204    fn eval_bad_script() {
205        assert!(eval("s/[invalid/x/", "test").is_err());
206    }
207
208    #[test]
209    fn sed_builder_quiet() {
210        let output = Sed::new("2p")
211            .unwrap()
212            .quiet(true)
213            .eval("a\nb\nc\n")
214            .unwrap();
215        assert_eq!(output, "b\n");
216    }
217
218    #[test]
219    fn sed_builder_stream() {
220        let sed = Sed::new("s/hello/world/").unwrap();
221        let input = b"hello\n";
222        let mut output = Vec::new();
223        sed.eval_stream(&input[..], &mut output).unwrap();
224        assert_eq!(String::from_utf8(output).unwrap(), "world\n");
225    }
226
227    #[test]
228    fn sed_reuse() {
229        let sed = Sed::new("s/x/y/g").unwrap();
230        assert_eq!(sed.eval("xxx\n").unwrap(), "yyy\n");
231        assert_eq!(sed.eval("axa\n").unwrap(), "aya\n");
232    }
233}