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}