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
20 changes: 20 additions & 0 deletions src/sed/fast_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,14 @@ impl OutputBuffer {
}
}

/// Continue the output of `previous`, which wrote to the same stream,
/// including a newline it deferred.
#[must_use]
pub fn continuing(mut self, previous: &Self) -> Self {
self.pending_newline = previous.pending_newline;
self
}

/// Schedule the specified String or &str for eventual output
pub fn write_str<S: Into<String>>(&mut self, s: S) -> io::Result<()> {
let mut s = s.into();
Expand Down Expand Up @@ -692,7 +700,19 @@ impl OutputBuffer {
// As in GNU sed, write the deferred newline even when the file is
// empty or cannot be read.
self.write_raw(b"")?;
self.copy_file_contents(path)
}

/// Copy the specified file to the output, leaving any deferred newline
/// for the next output, as GNU sed does for `0r`.
pub fn copy_file_before_pending_newline(&mut self, path: &PathBuf) -> io::Result<()> {
// Flush mmap writes, if any.
#[cfg(unix)]
self.flush_mmap(WriteRange::Complete)?;
self.copy_file_contents(path)
}

fn copy_file_contents(&mut self, path: &PathBuf) -> io::Result<()> {
let Ok(file) = File::open(path) else {
// Per POSIX, if the file can't be read treat it as empty.
return Ok(());
Expand Down
2 changes: 1 addition & 1 deletion src/sed/in_place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ 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()));
self.output = OutputBuffer::new(Box::new(stdout())).continuing(&self.output);
return Ok(&mut self.output);
}

Expand Down
67 changes: 51 additions & 16 deletions src/sed/named_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,29 @@ 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);
(if pending {
self.writer.write_all(b"\n")
} else {
Ok(())
})
.and_then(|()| 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()
})
}

/// Flush the writer, returning a descriptive error.
Expand Down Expand Up @@ -138,4 +146,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()));
}
}
17 changes: 6 additions & 11 deletions src/sed/processor.rs
Original file line number Diff line number Diff line change
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_before_pending_newline(path)?;
}

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