diff --git a/src/sed/command.rs b/src/sed/command.rs index fbf335ee..06f62fcc 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 { @@ -394,6 +401,8 @@ pub struct InputAction { pub next_command: Option>>, /// Data to prepend to the read contents pub prepend: Vec, + /// True if the prepended line ended with a line separator + pub prepend_terminated: bool, } #[cfg(test)] 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..789be5c0 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(), } } @@ -290,7 +290,7 @@ impl<'a> IOChunk<'a> { IOChunkContent::MmapInput { content, full_span, .. } => { - let has_newline = full_span.last().copied() == Some(b'\n'); + let has_newline = full_span.len() > content.len(); self.content = IOChunkContent::new_owned(content.to_vec(), has_newline); self.utf8_verified.set(false); Ok(()) @@ -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(); @@ -667,14 +703,18 @@ impl OutputBuffer { /// Schedule the specified bytes for eventual output. pub fn write_bytes(&mut self, bytes: &[u8]) -> io::Result<()> { - let (content, has_newline) = if bytes.ends_with(b"\n") { - (&bytes[..bytes.len() - 1], true) - } else { - (bytes, false) - }; + match bytes.strip_suffix(b"\n") { + Some(content) => self.write_line(content, true), + None => self.write_line(bytes, false), + } + } + + /// Schedule a line for output, followed by the terminator if + /// `terminated` is true. + pub fn write_line(&mut self, content: &[u8], terminated: bool) -> io::Result<()> { self.write_chunk(&IOChunk::from_content(IOChunkContent::new_owned( content.to_vec(), - has_newline, + terminated, ))) } @@ -727,7 +767,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 +827,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 +882,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 +904,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 +916,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 +927,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 +1575,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 +1656,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 +2068,7 @@ mod tests { #[cfg(unix)] mmap_chunk: None, pending_newline: false, + terminator: b'\n', low_level_flushes: 0, }; (buf, file) @@ -1993,7 +2085,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 +2244,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/in_place.rs b/src/sed/in_place.rs index 50c1faf8..0af019d0 100644 --- a/src/sed/in_place.rs +++ b/src/sed/in_place.rs @@ -67,7 +67,8 @@ impl InPlace { /// to the context settings. fn begin_resolved(&mut self, file_name: &Path) -> UResult<&mut OutputBuffer> { if !self.in_place { - self.output = OutputBuffer::new(Box::new(stdout())); + // Keep the stdout buffer across files, so that a separator + // missing at the end of one file is output before the next. return Ok(&mut self.output); } 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..f106149e 100644 --- a/src/sed/processor.rs +++ b/src/sed/processor.rs @@ -257,20 +257,21 @@ fn shell_stdout( } /// Execute the pattern space as a shell command, replacing its contents -/// with the command's standard output, minus one trailing newline. +/// with the command's standard output, minus one trailing line separator. fn execute_pattern_as_shell_command( pattern: &mut IOChunk, command: &Command, context: &mut ProcessingContext, ) -> UResult<()> { let mut shell_out = shell_stdout(pattern.as_bytes().to_vec(), command, context)?; + let separator = context.line_separator(); #[cfg(windows)] - if shell_out.ends_with(b"\r\n") { + if separator == b'\n' && shell_out.ends_with(b"\r\n") { // On Windows a trailing \r\n is the line terminator. Strip both. shell_out.truncate(shell_out.len() - 2); } - // Unix (and some Windows tools) end with a single \n. Strip it, as GNU sed does. - if shell_out.ends_with(b"\n") { + // Strip a single trailing separator (\n, or \0 with -z), as GNU sed does. + if shell_out.last() == Some(&separator) { shell_out.pop(); } pattern.set_to_bytes(shell_out, pattern.is_newline_terminated()); @@ -397,9 +398,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 +470,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 +545,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 +598,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 +616,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 +675,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 +740,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 +756,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 +778,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 +789,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(); } @@ -790,6 +814,7 @@ fn process_file( context.input_action = Some(InputAction { next_command: command.next.clone(), prepend: pattern.as_bytes().to_vec(), + prepend_terminated: pattern.is_newline_terminated(), }); continue 'lines; } @@ -798,8 +823,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)?; } @@ -818,8 +844,9 @@ fn process_file( i32::try_from(*extract_variant!(command, Number)).unwrap_or(i32::MAX), ); context.stop_processing = true; - context.quiet = true; - break; + // Like GNU sed, output nothing more: not the pattern + // space, appended text, or a missing line separator. + return Ok(()); } 'R' => { // Queue the file's next line for output at end of cycle. @@ -874,23 +901,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' => { @@ -945,9 +976,7 @@ fn process_file( && let Some(action) = context.input_action.take() && !context.quiet { - let mut pending = action.prepend; - pending.push(b'\n'); - output.write_bytes(&pending)?; + output.write_line(&action.prepend, action.prepend_terminated)?; if context.unbuffered { output.flush()?; } @@ -986,7 +1015,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; @@ -1006,9 +1037,7 @@ pub fn process_all_files( && !context.quiet && let Some(action) = context.input_action.take() { - let mut pending = action.prepend; - pending.push(b'\n'); - output.write_bytes(&pending)?; + output.write_line(&action.prepend, action.prepend_terminated)?; } in_place.end()?; @@ -1110,6 +1139,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 +1180,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..ac888f76 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -1432,7 +1432,7 @@ fn test_uppercase_delete_prevents_automatic_printing() { .args(&["-e", "N", "-e", "D"]) .pipe_in("line1\nline2\nline3") .succeeds() - .stdout_is("line3\n"); + .stdout_is("line3"); } //////////////////////////////////////////////////////////// @@ -3012,3 +3012,217 @@ fn test_posix_reject_flags() { .code_is(1) .stderr_is("sed: