Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/sed/fast_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ pub struct OutputBuffer {
mmap_chunk: Option<MmapOutput>, // Chunk to write
// 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,
pub(crate) pending_newline: bool,
#[cfg(test)]
low_level_flushes: usize, // Number of system call flushes
}
Expand Down Expand Up @@ -687,11 +687,17 @@ impl OutputBuffer {
self.out.write_all(bytes)
}

/// Copy the specified file to the output, after any deferred newline.
pub fn copy_file(&mut self, path: &PathBuf) -> io::Result<()> {
// As in GNU sed, write the deferred newline even when the file is
// empty or cannot be read.
self.write_raw(b"")?;
/// Copy the specified file to the output. With `after_newline`, write
/// any deferred newline first, even when the file is empty or cannot be
/// read, as GNU sed does; otherwise leave it for the next output, as GNU
/// sed does for `0r`.
pub fn copy_file(&mut self, path: &PathBuf, after_newline: bool) -> io::Result<()> {
if after_newline {
self.flush_pending_newline()?;
}
// Flush mmap writes, if any.
#[cfg(unix)]
self.flush_mmap(WriteRange::Complete)?;

let Ok(file) = File::open(path) else {
// Per POSIX, if the file can't be read treat it as empty.
Expand Down
3 changes: 3 additions & 0 deletions src/sed/in_place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ impl InPlace {
/// to the context settings.
fn begin_resolved(&mut self, file_name: &Path) -> UResult<&mut OutputBuffer> {
if !self.in_place {
// Carry over a newline deferred at the end of the previous file.
let pending_newline = self.output.pending_newline;
self.output = OutputBuffer::new(Box::new(stdout()));
self.output.pending_newline = pending_newline;
return Ok(&mut self.output);
}

Expand Down
66 changes: 49 additions & 17 deletions src/sed/named_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::sed::error_handling::{ScriptLocation, runtime_error};

use std::cell::RefCell;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::io::{self, BufWriter, Write};
use std::path::PathBuf;
use std::rc::Rc;

Expand All @@ -30,6 +30,7 @@ pub struct NamedWriter {
pub path: PathBuf,
writer: BufWriter<File>,
location: ScriptLocation,
pending_newline: bool, // The last line written lacked its newline
}

impl NamedWriter {
Expand All @@ -49,6 +50,7 @@ impl NamedWriter {
path,
writer: BufWriter::new(file),
location,
pending_newline: false,
}));

FLUSH_LIST.with(|list| list.borrow_mut().push(Rc::clone(&writer)));
Expand All @@ -61,23 +63,26 @@ impl NamedWriter {
}

/// Write bytes to the file, possibly with a newline, returning errors.
/// A missing newline is written before any further line.
pub fn write_line_bytes(&mut self, line: &[u8], newline: bool) -> UResult<()> {
self.writer
.write_all(line)
.and_then(|()| {
if newline {
self.writer.write_all(b"\n")
} else {
Ok(())
}
})
.map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {e}", self.path.quote()),
)
.unwrap_err()
})
let pending = std::mem::replace(&mut self.pending_newline, !newline);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this chain of if/and_then is hard to read
could you please use a small closure with ? and do the map_err once?

let mut write = || -> io::Result<()> {
if pending {
self.writer.write_all(b"\n")?;
}
self.writer.write_all(line)?;
if newline {
self.writer.write_all(b"\n")?;
}
Ok(())
};
write().map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {e}", self.path.quote()),
)
.unwrap_err()
})
}

/// Flush the writer, returning a descriptive error.
Expand Down Expand Up @@ -138,4 +143,31 @@ mod tests {

assert_eq!(fs::read(path).unwrap(), b"a\xE9");
}

#[test]
fn test_write_line_bytes_reports_deferred_newline_error() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let mut writer = NamedWriter {
path: path.clone(),
// Unbuffered writes to a read-only handle fail immediately.
writer: BufWriter::with_capacity(0, File::open(&path).unwrap()),
location: ScriptLocation {
input_name: Rc::from("script.sed"),
line_number: 3,
column_number: 7,
..ScriptLocation::default()
},
pending_newline: false,
};

writer.write_line_bytes(b"", false).unwrap();
// Only the deferred newline has bytes to write.
let error = writer.write_line_bytes(b"", false).unwrap_err();
assert_eq!(error.code(), 2);
let message = error.to_string();
assert!(message.starts_with("script.sed:3:7: error: "));
assert!(message.contains("writing to file"));
assert!(message.contains(path.to_str().unwrap()));
}
}
19 changes: 7 additions & 12 deletions src/sed/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ fn flush_appends(output: &mut OutputBuffer, context: &mut ProcessingContext) ->
output.write_raw(text.as_ref())?;
}
AppendElement::Path(path) => {
output.copy_file(path)?;
output.copy_file(path, true)?;
}
}
}
Expand Down Expand Up @@ -632,7 +632,7 @@ fn process_address_0(
&& cmd.addr2.is_none()
{
let path = extract_variant!(cmd, Path);
output.copy_file(path)?;
output.copy_file(path, false)?;
}

cmd.next.clone()
Expand Down Expand Up @@ -902,16 +902,11 @@ fn process_file(
// Append only the first line of the pattern space.
let writer = extract_variant!(command, NamedWriter);
let pattern_bytes = pattern.as_bytes();
let (first_line, found_newline) =
match pattern_bytes.iter().position(|&b| b == b'\n') {
// 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(),
)?;
let (first_line, newline) = match memchr(b'\n', pattern_bytes) {
Some(pos) => (&pattern_bytes[..pos], true),
None => (pattern_bytes, pattern.is_newline_terminated()),
};
writer.borrow_mut().write_line_bytes(first_line, newline)?;
}
'x' => {
// Exchange the contents of the pattern and hold spaces.
Expand Down
63 changes: 61 additions & 2 deletions tests/by-util/test_sed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2020,12 +2020,12 @@ fn write_first_line_newline() -> std::io::Result<()> {

new_ucmd!()
.args(&["-n", "-e", &cmd])
.pipe_in("abc\ndef\n")
.pipe_in("abc\ndef\nghi\njkl\n")
.succeeds();

let mut actual = String::new();
temp.reopen()?.read_to_string(&mut actual)?;
assert_eq!(actual, "abc\n");
assert_eq!(actual, "abc\nghi\n");

Ok(())
}
Expand Down Expand Up @@ -2618,6 +2618,8 @@ fn in_place_edit_separate() -> std::io::Result<()> {
"b1+b2\n",
),
(&["-n", "-i", "-e", "N;p"], "a1\n", b, "", "b1\nb2\n"),
// A newline missing at the end of one file is not written to the next.
(&["-i", "-e", "s/a/A/"], "a", "b\n", "A", "b\n"),
];

for (args, first, second, expected1, expected2) in cases {
Expand Down Expand Up @@ -3408,3 +3410,60 @@ fn test_execute_command_output_is_copied_unchanged() {
.succeeds()
.stdout_is("a\n");
}

/// A newline that a file's last line lacks is written before the next
/// file's output, both to standard output and to `w` and `W` files.
#[test]
fn test_missing_newline_is_written_between_files() -> std::io::Result<()> {
type Case = (&'static [&'static str], &'static str, [&'static str; 3]);
let cases: &[Case] = &[
(&[], "w", ["e\nf", "g\nh", "e\nf\ng\nh"]),
(&[], "W", ["e\nf", "g\nh", "e\nf\ng\nh"]),
(&["-s"], "w", ["e\nf", "g\nh", "e\nf\ng\nh"]),
(&[], "2Q;w", ["x", "y", "x"]),
];
for (flags, script, [first, second, expected]) in cases {
let (dir, one, two) = two_inputs(first, second)?;
let out = dir.path().join("out");
new_ucmd!()
.args(flags)
.arg(format!("{script} {}", out.display()))
.arg(&one)
.arg(&two)
.succeeds()
.stdout_is(*expected);
assert_eq!(fs::read_to_string(&out)?, *expected, "{flags:?} {script}");
}
Ok(())
}

/// `0r` at the start of the next file leaves the missing newline for that
/// file's first line, as in GNU sed.
#[test]
fn test_zero_address_read_keeps_missing_newline_for_next_line() -> std::io::Result<()> {
let (dir, one, two) = two_inputs("a", "b\n")?;
let text = dir.path().join("text");
fs::write(&text, "X\nY")?;
new_ucmd!()
.args(&["-s", &format!("0r {}", text.display())])
.arg(&one)
.arg(&two)
.succeeds()
.stdout_is("X\nYaX\nY\nb\n");
Ok(())
}

/// A `w` file gets the newline that an emptied last line lacks before the
/// next file's line, as in GNU sed.
#[test]
fn test_write_empty_line_without_newline() -> std::io::Result<()> {
let (dir, one, two) = two_inputs("a", "b")?;
let out = dir.path().join("out");
new_ucmd!()
.args(&["-n", &format!("s/.*//w {}", out.display())])
.arg(&one)
.arg(&two)
.succeeds();
assert_eq!(fs::read_to_string(&out)?, "\n");
Ok(())
}
Loading
Loading