From ed1bc2c336e2538ccb00712962aeb06f4803dea7 Mon Sep 17 00:00:00 2001 From: sap1110 <238814652+sap1110@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:53:37 +0530 Subject: [PATCH 1/2] sed: separate lines with NUL when -z is given The -z (--null-data) option was accepted but ignored. Keep the line separator in the processing context and use it to split input lines (including those read by R), to terminate output lines (including those of =, F, i, c, l and the w and W files), and to join lines in N, G and H and split them in D, P and W, as GNU sed does. As in GNU sed, text appended by a, and the output of e, are written unchanged, and l lists a newline in the pattern space as \n. Fixes #530 --- src/sed/command.rs | 7 ++ src/sed/compiler.rs | 3 +- src/sed/fast_io.rs | 166 +++++++++++++++++++++++++++++++++----- src/sed/named_reader.rs | 28 +++++-- src/sed/named_writer.rs | 34 +++++--- src/sed/processor.rs | 91 ++++++++++++++++----- tests/by-util/test_sed.rs | 131 ++++++++++++++++++++++++++++++ 7 files changed, 404 insertions(+), 56 deletions(-) diff --git a/src/sed/command.rs b/src/sed/command.rs index fbf335ee..b5f10d3e 100644 --- a/src/sed/command.rs +++ b/src/sed/command.rs @@ -79,6 +79,13 @@ pub struct ProcessingContext { pub append_elements: Vec, } +impl ProcessingContext { + /// Return the byte that separates lines: \0 with -z, otherwise \n. + pub fn line_separator(&self) -> u8 { + if self.null_data { b'\0' } else { b'\n' } + } +} + #[derive(Clone, Debug)] /// Elements that shall be appended at the end of each command processing cycle pub enum AppendElement { diff --git a/src/sed/compiler.rs b/src/sed/compiler.rs index 02184fb2..129c11be 100644 --- a/src/sed/compiler.rs +++ b/src/sed/compiler.rs @@ -1143,10 +1143,11 @@ fn compile_read_line_command( let path = read_file_path(lines, line)?; // Reuse a reader keyed by path so all `R` commands on the same file share // one cursor (see `ProcessingContext::named_readers`). + let separator = context.line_separator(); let reader = context .named_readers .entry(path.clone()) - .or_insert_with(|| NamedReader::new(path)) + .or_insert_with(|| NamedReader::new(path, separator)) .clone(); cmd.data = CommandData::NamedReader(reader); Ok(CommandHandling::Continue) diff --git a/src/sed/fast_io.rs b/src/sed/fast_io.rs index 5ea8bf3d..3ced97bf 100644 --- a/src/sed/fast_io.rs +++ b/src/sed/fast_io.rs @@ -58,6 +58,7 @@ pub struct MmapLineCursor<'a> { fast_copy: FastCopy, // Data for fast file copy I/O data: &'a [u8], // Mmapped data pos: usize, // Position within the data + separator: u8, // Input record separator } #[cfg(unix)] @@ -75,6 +76,7 @@ impl<'a> MmapLineCursor<'a> { _file: file, data, pos: 0, + separator: b'\n', } } @@ -86,19 +88,19 @@ impl<'a> MmapLineCursor<'a> { let start = self.pos; - let mut end = if let Some(pos) = memchr(b'\n', &self.data[start..]) { + let mut end = if let Some(pos) = memchr(self.separator, &self.data[start..]) { pos + start } else { self.data.len() }; if end < self.data.len() { - end += 1; // include \n in full span + end += 1; // include the separator in full span } self.pos = end; let full_span = &self.data[start..end]; - let content = if full_span.ends_with(b"\n") { + let content = if full_span.last() == Some(&self.separator) { &full_span[..full_span.len() - 1] } else { full_span @@ -117,6 +119,7 @@ impl<'a> MmapLineCursor<'a> { pub struct ReadLineCursor { reader: Box, buffer: Vec, + separator: u8, // Input record separator } impl ReadLineCursor { @@ -126,19 +129,20 @@ impl ReadLineCursor { Self { reader: Box::new(buf), buffer: Vec::new(), + separator: b'\n', } } - /// If a line is available, return it and its \n termination. + /// If a line is available, return it and its separator termination. fn get_line(&mut self) -> io::Result, bool)>> { self.buffer.clear(); - // read_line *includes* the '\n' if present - let bytes_read = self.reader.read_until(b'\n', &mut self.buffer)?; + // read_until *includes* the separator if present + let bytes_read = self.reader.read_until(self.separator, &mut self.buffer)?; if bytes_read == 0 { return Ok(None); } - // O(1) check whether it ended in '\n' - let has_newline = self.buffer.ends_with(b"\n"); + // O(1) check whether it ended in the separator + let has_newline = self.buffer.last() == Some(&self.separator); // strip it if you don’t want to expose it to the caller if has_newline { self.buffer.pop(); @@ -200,13 +204,9 @@ impl<'a> IOChunk<'a> { match &self.content { IOChunkContent::Owned { has_newline, .. } => *has_newline, #[cfg(unix)] - IOChunkContent::MmapInput { full_span, .. } => { - if let Some(&last) = full_span.last() { - last == b'\n' - } else { - false - } - } + IOChunkContent::MmapInput { + full_span, content, .. + } => full_span.len() > content.len(), } } @@ -503,6 +503,19 @@ impl<'a> LineReader<'a> { line_reader_read_input(file) } + /// Set the byte that separates input records (lines). + pub fn set_separator(&mut self, separator: u8) { + match self { + #[cfg(unix)] + LineReader::MmapInput { cursor, .. } => cursor.separator = separator, + + LineReader::ReadInput(cursor) => cursor.separator = separator, + + #[cfg(not(unix))] + LineReader::_Phantom(_) => unreachable!("_Phantom should never be constructed"), + } + } + /// Return the next line, if available. pub fn get_line(&mut self) -> io::Result>> { match self { @@ -595,6 +608,7 @@ pub struct OutputBuffer { // True when the last write didn't end with \n; the \n is deferred so // that commands like `p` don't emit a spurious newline under -n. pending_newline: bool, + terminator: u8, // Output record terminator (\n, or \0 with -z) #[cfg(test)] low_level_flushes: usize, // Number of system call flushes } @@ -628,6 +642,7 @@ impl OutputBuffer { Self { out: BufWriter::new(w), pending_newline: false, + terminator: b'\n', #[cfg(test)] low_level_flushes: 0, } @@ -647,11 +662,32 @@ impl OutputBuffer { max_pending_write, mmap_chunk: None, pending_newline: false, + terminator: b'\n', #[cfg(test)] low_level_flushes: 0, } } + /// Set the byte written to terminate output records. + // A trailing \n in text passed to write_str and write_bytes is + // output as this terminator. + pub fn set_terminator(&mut self, terminator: u8) { + self.terminator = terminator; + } + + /// Schedule the specified bytes for output exactly as given. + pub fn write_raw(&mut self, bytes: &[u8]) -> io::Result<()> { + if bytes.is_empty() { + return Ok(()); + } + self.write_chunk(&IOChunk::from_content(IOChunkContent::new_owned( + bytes.to_vec(), + false, + )))?; + self.pending_newline = false; + Ok(()) + } + /// Schedule the specified String or &str for eventual output pub fn write_str>(&mut self, s: S) -> io::Result<()> { let mut s = s.into(); @@ -727,7 +763,7 @@ impl OutputBuffer { if self.pending_newline { self.flush_mmap(WriteRange::Complete)?; - self.out.write_all(b"\n")?; + self.out.write_all(&[self.terminator])?; self.pending_newline = false; } @@ -787,7 +823,7 @@ impl OutputBuffer { self.flush_mmap(WriteRange::Complete)?; self.out.write_all(content)?; if *has_newline { - self.out.write_all(b"\n")?; + self.out.write_all(&[self.terminator])?; } self.pending_newline = !has_newline; } @@ -842,7 +878,7 @@ impl OutputBuffer { pub fn flush_pending_newline(&mut self) -> io::Result<()> { if self.pending_newline { self.flush_mmap(WriteRange::Complete)?; - self.out.write_all(b"\n")?; + self.out.write_all(&[self.terminator])?; self.pending_newline = false; } Ok(()) @@ -864,7 +900,7 @@ impl OutputBuffer { } if self.pending_newline { - self.out.write_all(b"\n")?; + self.out.write_all(&[self.terminator])?; self.pending_newline = false; } @@ -876,7 +912,7 @@ impl OutputBuffer { } => { self.out.write_all(content)?; if *has_newline { - self.out.write_all(b"\n")?; + self.out.write_all(&[self.terminator])?; } self.pending_newline = !has_newline; Ok(()) @@ -887,7 +923,7 @@ impl OutputBuffer { /// Write a deferred newline if the last output didn't end with one. pub fn flush_pending_newline(&mut self) -> io::Result<()> { if self.pending_newline { - self.out.write_all(b"\n")?; + self.out.write_all(&[self.terminator])?; self.pending_newline = false; } Ok(()) @@ -1535,6 +1571,48 @@ mod tests { Ok(()) } + /// Read "a\0b\nc\0d" with a NUL separator and return its lines. + fn read_nul_separated(reader: &mut LineReader) -> io::Result, bool)>> { + reader.set_separator(b'\0'); + let mut lines = Vec::new(); + while let Some(line) = reader.get_line()? { + lines.push((line.as_bytes().to_vec(), line.is_newline_terminated())); + } + Ok(lines) + } + + const NUL_SEPARATED: &[u8] = b"a\0b\nc\0d"; + + fn nul_separated_lines() -> Vec<(Vec, bool)> { + vec![ + (b"a".to_vec(), true), + (b"b\nc".to_vec(), true), + (b"d".to_vec(), false), + ] + } + + #[test] + fn test_stream_read_nul_separator() -> io::Result<()> { + let mut tmp = NamedTempFile::new()?; + tmp.write_all(NUL_SEPARATED)?; + let mut reader = LineReader::open_stream(&tmp.path().to_path_buf())?; + + assert_eq!(read_nul_separated(&mut reader)?, nul_separated_lines()); + Ok(()) + } + + #[test] + #[cfg(unix)] + fn test_mmap_read_nul_separator() -> io::Result<()> { + let mut tmp = NamedTempFile::new()?; + tmp.write_all(NUL_SEPARATED)?; + let mut reader = LineReader::open(&tmp.path().to_path_buf())?; + assert!(matches!(reader, LineReader::MmapInput { .. })); + + assert_eq!(read_nul_separated(&mut reader)?, nul_separated_lines()); + Ok(()) + } + // is_newline_terminated, is_empty #[test] fn test_owned_newline_terminated_non_empty() { @@ -1574,6 +1652,15 @@ mod tests { assert!(!chunk.is_newline_terminated()); } + #[cfg(unix)] + #[test] + fn test_mmap_nul_terminated() { + let content = b"line"; + let full_span = b"line\0"; + let chunk = IOChunk::from_content(new_content_mmap_input(content, full_span)); + assert!(chunk.is_newline_terminated()); + } + #[cfg(unix)] #[test] fn test_mmap_empty() { @@ -1977,6 +2064,7 @@ mod tests { #[cfg(unix)] mmap_chunk: None, pending_newline: false, + terminator: b'\n', low_level_flushes: 0, }; (buf, file) @@ -1993,7 +2081,8 @@ mod tests { block_size: 1, }, base: bytes.as_ptr(), - content: bytes, + // As returned by get_line: without the line's newline. + content: bytes.strip_suffix(b"\n").unwrap_or(bytes), full_span: bytes, }, } @@ -2151,4 +2240,37 @@ mod tests { file.read_to_string(&mut out).unwrap(); assert_eq!(out, "baz\n"); } + + // A set terminator replaces the \n that ends each output line + #[test] + fn nul_terminator_ends_lines() { + let (mut buf, mut file) = new_for_test(); + buf.set_terminator(b'\0'); + buf.write_str("a\n").unwrap(); + buf.write_bytes(b"b").unwrap(); + buf.write_chunk(&make_owned_chunk("c", false)).unwrap(); + buf.flush_pending_newline().unwrap(); + buf.out.flush().unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + let mut out = String::new(); + file.read_to_string(&mut out).unwrap(); + assert_eq!(out, "a\0b\0c\0"); + } + + // write_raw outputs its bytes unchanged and completes the line + #[test] + fn write_raw_outputs_bytes_unchanged() { + let (mut buf, mut file) = new_for_test(); + buf.set_terminator(b'\0'); + buf.write_str("a").unwrap(); + buf.write_raw(b"").unwrap(); + buf.write_raw(b"b\n").unwrap(); + assert!(!buf.pending_newline); + buf.write_str("c\n").unwrap(); + buf.out.flush().unwrap(); + file.seek(SeekFrom::Start(0)).unwrap(); + let mut out = String::new(); + file.read_to_string(&mut out).unwrap(); + assert_eq!(out, "a\0b\nc\0"); + } } diff --git a/src/sed/named_reader.rs b/src/sed/named_reader.rs index 58a32c15..a4dc0729 100644 --- a/src/sed/named_reader.rs +++ b/src/sed/named_reader.rs @@ -29,18 +29,20 @@ enum State { pub struct NamedReader { path: PathBuf, state: State, + separator: u8, // Line separator (\n, or \0 with -z) } impl NamedReader { /// Create a reader for `path` without opening it yet. - pub fn new(path: PathBuf) -> Rc> { + pub fn new(path: PathBuf, separator: u8) -> Rc> { Rc::new(RefCell::new(NamedReader { path, state: State::Unopened, + separator, })) } - /// Return the next line of the file, including its trailing newline if + /// Return the next line of the file, including its trailing separator if /// present, or `None` once the file is exhausted or could not be read. pub fn next_line(&mut self) -> Option> { if matches!(self.state, State::Unopened) { @@ -55,7 +57,7 @@ impl NamedReader { }; let mut line = Vec::new(); - match reader.read_until(b'\n', &mut line) { + match reader.read_until(self.separator, &mut line) { Ok(0) | Err(_) => { self.state = State::Exhausted; None @@ -75,7 +77,7 @@ mod tests { fn yields_successive_lines_then_none() { let mut file = NamedTempFile::new().unwrap(); file.write_all(b"one\ntwo\n").unwrap(); - let reader = NamedReader::new(file.path().to_path_buf()); + let reader = NamedReader::new(file.path().to_path_buf(), b'\n'); assert_eq!(reader.borrow_mut().next_line(), Some(b"one\n".to_vec())); assert_eq!(reader.borrow_mut().next_line(), Some(b"two\n".to_vec())); @@ -87,7 +89,7 @@ mod tests { fn last_line_without_newline_is_preserved() { let mut file = NamedTempFile::new().unwrap(); file.write_all(b"abc").unwrap(); - let reader = NamedReader::new(file.path().to_path_buf()); + let reader = NamedReader::new(file.path().to_path_buf(), b'\n'); assert_eq!(reader.borrow_mut().next_line(), Some(b"abc".to_vec())); assert_eq!(reader.borrow_mut().next_line(), None); @@ -95,7 +97,21 @@ mod tests { #[test] fn missing_file_yields_no_lines() { - let reader = NamedReader::new(PathBuf::from("/nonexistent/xyzzy-42-does-not-exist")); + let reader = NamedReader::new(PathBuf::from("/nonexistent/xyzzy-42-does-not-exist"), b'\n'); + assert_eq!(reader.borrow_mut().next_line(), None); + } + + #[test] + fn nul_separator_splits_lines() { + let mut file = NamedTempFile::new().unwrap(); + file.write_all(b"one\0two\nthree").unwrap(); + let reader = NamedReader::new(file.path().to_path_buf(), b'\0'); + + assert_eq!(reader.borrow_mut().next_line(), Some(b"one\0".to_vec())); + assert_eq!( + reader.borrow_mut().next_line(), + Some(b"two\nthree".to_vec()) + ); assert_eq!(reader.borrow_mut().next_line(), None); } } diff --git a/src/sed/named_writer.rs b/src/sed/named_writer.rs index 18cf1e5c..cd07449d 100644 --- a/src/sed/named_writer.rs +++ b/src/sed/named_writer.rs @@ -55,18 +55,19 @@ impl NamedWriter { Ok(writer) } - /// Write String to the file, possibly with a newline, returning errors. - pub fn write_line(&mut self, line: &str, newline: bool) -> UResult<()> { - self.write_line_bytes(line.as_bytes(), newline) + /// Write String to the file, possibly with a terminator, returning errors. + pub fn write_line(&mut self, line: &str, terminator: Option) -> UResult<()> { + self.write_line_bytes(line.as_bytes(), terminator) } - /// Write bytes to the file, possibly with a newline, returning errors. - pub fn write_line_bytes(&mut self, line: &[u8], newline: bool) -> UResult<()> { + /// Write bytes to the file, possibly with a terminator (\n, or \0 with + /// -z), returning errors. + pub fn write_line_bytes(&mut self, line: &[u8], terminator: Option) -> UResult<()> { self.writer .write_all(line) .and_then(|()| { - if newline { - self.writer.write_all(b"\n") + if let Some(terminator) = terminator { + self.writer.write_all(&[terminator]) } else { Ok(()) } @@ -117,7 +118,7 @@ mod tests { writer .borrow_mut() - .write_line_bytes(b"a\xE9", true) + .write_line_bytes(b"a\xE9", Some(b'\n')) .unwrap(); writer.borrow_mut().flush().unwrap(); @@ -132,10 +133,25 @@ mod tests { writer .borrow_mut() - .write_line_bytes(b"a\xE9", false) + .write_line_bytes(b"a\xE9", None) .unwrap(); writer.borrow_mut().flush().unwrap(); assert_eq!(fs::read(path).unwrap(), b"a\xE9"); } + + #[test] + fn test_write_line_bytes_appends_nul() { + let file = NamedTempFile::new().unwrap(); + let path = file.path().to_path_buf(); + let writer = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap(); + + writer + .borrow_mut() + .write_line_bytes(b"a\nb", Some(b'\0')) + .unwrap(); + writer.borrow_mut().flush().unwrap(); + + assert_eq!(fs::read(path).unwrap(), b"a\nb\0"); + } } diff --git a/src/sed/processor.rs b/src/sed/processor.rs index 3f0ebb8b..6a47d021 100644 --- a/src/sed/processor.rs +++ b/src/sed/processor.rs @@ -397,9 +397,12 @@ fn substitute( // Write to file if needed. if let Some(ref writer) = sub.write_file { - writer - .borrow_mut() - .write_line_bytes(pattern.as_bytes(), pattern.is_newline_terminated())?; + writer.borrow_mut().write_line_bytes( + pattern.as_bytes(), + pattern + .is_newline_terminated() + .then_some(context.line_separator()), + )?; } context.substitution_made = true; } @@ -466,6 +469,11 @@ fn transliterate( fn flush_appends(output: &mut OutputBuffer, context: &mut ProcessingContext) -> UResult<()> { for elem in &context.append_elements { match elem { + // With -z, GNU sed outputs appended text as is: `a` text keeps + // its trailing \n and `R` lines keep their \0. + AppendElement::Text(text) if context.null_data => { + output.write_raw(text.as_ref())?; + } AppendElement::Text(text) => { output.write_bytes(text.as_ref())?; } @@ -536,6 +544,16 @@ impl ListLine { Ok(()) } + /// Write a newline of the pattern space. With -z it is data, so it is + /// listed as an escape rather than ending the list line. + fn write_newline(&mut self, output: &mut OutputBuffer, null_data: bool) -> UResult<()> { + if null_data { + self.write_item(output, r"\n") + } else { + self.write_embedded_newline(output) + } + } + /// Write the current list line with an embedded newline marker. fn write_embedded_newline(&mut self, output: &mut OutputBuffer) -> UResult<()> { self.buffer.push_str("$\n"); @@ -579,7 +597,7 @@ fn list( // List non-ASCII bytes in octal. for &byte in line.as_bytes() { if byte == b'\n' { - list_line.write_embedded_newline(output)?; + list_line.write_newline(output, context.null_data)?; continue; } let out_str = readable_ascii_byte(byte); @@ -597,7 +615,7 @@ fn list( })?; for ch in line.chars() { if ch == '\n' { - list_line.write_embedded_newline(output)?; + list_line.write_newline(output, context.null_data)?; continue; } let out_str = readable_char(ch); @@ -656,7 +674,7 @@ fn process_file( if let Some(action) = context.input_action.take() { // Continue processing the `N` command. let mut combined_lines = action.prepend; - combined_lines.push(b'\n'); + combined_lines.push(context.line_separator()); combined_lines.extend_from_slice(pattern.as_bytes()); pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated()); @@ -721,7 +739,7 @@ fn process_file( } 'D' => { // Delete up to \n and start a new cycle without new input. - if let Some(pos) = memchr(b'\n', pattern.as_bytes()) { + if let Some(pos) = memchr(context.line_separator(), pattern.as_bytes()) { let (s, _) = pattern.fields_mut()?; s.drain(..=pos); current.clone_from(&commands); @@ -737,7 +755,12 @@ fn process_file( } CommandData::Text(cmd_bytes) => { let shell_out = shell_stdout(cmd_bytes.to_vec(), &command, context)?; - output.write_bytes(&shell_out)?; + if context.null_data { + // GNU sed outputs the command's \n unchanged. + output.write_raw(&shell_out)?; + } else { + output.write_bytes(&shell_out)?; + } } _ => panic!("invalid 'e' command data"), }, @@ -754,7 +777,7 @@ fn process_file( 'G' => { // Append to pattern \n followed by hold space contents. let (pat_content, pat_has_newline) = pattern.fields_mut()?; - pat_content.push(b'\n'); + pat_content.push(context.line_separator()); pat_content.extend_from_slice(&context.hold.content); *pat_has_newline = context.hold.has_newline; } @@ -765,7 +788,7 @@ fn process_file( } 'H' => { // Append to hold \n followed by pattern space contents. - context.hold.content.push(b'\n'); + context.hold.content.push(context.line_separator()); context.hold.content.extend_from_slice(pattern.as_bytes()); context.hold.has_newline = pattern.is_newline_terminated(); } @@ -798,8 +821,9 @@ fn process_file( } 'P' => { let line = pattern.as_bytes(); - if let Some(pos) = memchr(b'\n', line) { - output.write_bytes(&line[..=pos])?; + if let Some(pos) = memchr(context.line_separator(), line) { + // The trailing \n is output as the line terminator. + output.write_bytes(&[&line[..pos], b"\n"].concat())?; } else { write_chunk(output, context, &pattern)?; } @@ -874,23 +898,27 @@ fn process_file( 'w' => { // Append the pattern space to the specified file. let writer = extract_variant!(command, NamedWriter); - writer - .borrow_mut() - .write_line_bytes(pattern.as_bytes(), pattern.is_newline_terminated())?; + writer.borrow_mut().write_line_bytes( + pattern.as_bytes(), + pattern + .is_newline_terminated() + .then_some(context.line_separator()), + )?; } 'W' => { // Append only the first line of the pattern space. let writer = extract_variant!(command, NamedWriter); + let separator = context.line_separator(); let pattern_bytes = pattern.as_bytes(); let (first_line, found_newline) = - match pattern_bytes.iter().position(|&b| b == b'\n') { + match pattern_bytes.iter().position(|&b| b == separator) { // A slice including the newline Some(pos) => (&pattern_bytes[..=pos], true), None => (pattern_bytes, false), }; writer.borrow_mut().write_line_bytes( first_line, - !found_newline && pattern.is_newline_terminated(), + (!found_newline && pattern.is_newline_terminated()).then_some(separator), )?; } 'x' => { @@ -986,7 +1014,9 @@ pub fn process_all_files( context.last_file = index == last_file_index; let mut reader = LineReader::open(&path) .map_err_context(|| format!("error opening input file {}", path.quote()))?; + reader.set_separator(context.line_separator()); let output = in_place.begin(&path)?; + output.set_terminator(context.line_separator()); if context.separate || index == 0 { context.line_number = 0; @@ -1110,6 +1140,31 @@ mod tests { assert_eq!(written, "abc$\n"); } + #[test] + fn test_list_null_data_escapes_newline() { + let mut file = tempfile().unwrap(); + let mut output = OutputBuffer::new(Box::new(file.try_clone().unwrap())); + let context = ProcessingContext { + null_data: true, + ..ProcessingContext::default() + }; + + list( + &mut output, + &IOChunk::new_from_str("a\nb"), + 70, + &ScriptLocation::default(), + &context, + ) + .unwrap(); + output.flush().unwrap(); + + file.seek(SeekFrom::Start(0)).unwrap(); + let mut written = String::new(); + file.read_to_string(&mut written).unwrap(); + assert_eq!(written, "a\\nb$\n"); + } + #[test] fn test_execute_read_line_command_queues_next_line() { use crate::sed::command::CommandData; @@ -1126,7 +1181,7 @@ mod tests { input_file.write_all(b"x\n").unwrap(); // One `R` command with no address, so it always applies. - let reader = NamedReader::new(read_file.path().to_path_buf()); + let reader = NamedReader::new(read_file.path().to_path_buf(), b'\n'); let command = Rc::new(RefCell::new(Command { code: 'R', data: CommandData::NamedReader(reader), diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index ace171d3..38c276ec 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -3012,3 +3012,134 @@ fn test_posix_reject_flags() { .code_is(1) .stderr_is("sed: