diff --git a/README.md b/README.md index 92036350..255573fa 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ cargo test * An `F` command outputs the name of the file currently being processed. * A `Q` command (optionally followed by an exit code) quits immediately. * The `q` command can be optionally followed by an exit code. +* A `W` command writes to a file the pattern's first line. * The `l` command can be optionally followed by the output width. * The `--follow-symlinks` option for in-place editing. * The `--sandbox` option that limits potentially destructive commands. diff --git a/src/sed/compiler.rs b/src/sed/compiler.rs index 457fd3d8..1ea7a8e1 100644 --- a/src/sed/compiler.rs +++ b/src/sed/compiler.rs @@ -1648,6 +1648,10 @@ fn get_cmd_spec( n_addr: 2, handler: compile_label_command, }), + 'W' if !posix => Ok(CommandSpec { + n_addr: 2, + handler: compile_write_file_command, + }), 'w' => Ok(CommandSpec { n_addr: 2, handler: compile_write_file_command, diff --git a/src/sed/named_writer.rs b/src/sed/named_writer.rs index 47450c8c..18cf1e5c 100644 --- a/src/sed/named_writer.rs +++ b/src/sed/named_writer.rs @@ -55,16 +55,22 @@ impl NamedWriter { Ok(writer) } - /// Write a line to the file with a newline, returning descriptive errors. - pub fn write_line(&mut self, line: &str) -> UResult<()> { - self.write_line_bytes(line.as_bytes()) + /// 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 bytes to the file with a newline, returning descriptive errors. - pub fn write_line_bytes(&mut self, line: &[u8]) -> UResult<()> { + /// Write bytes to the file, possibly with a newline, returning errors. + pub fn write_line_bytes(&mut self, line: &[u8], newline: bool) -> UResult<()> { self.writer .write_all(line) - .and_then(|()| self.writer.write_all(b"\n")) + .and_then(|()| { + if newline { + self.writer.write_all(b"\n") + } else { + Ok(()) + } + }) .map_err(|e| { runtime_error::<()>( &self.location, @@ -109,9 +115,27 @@ mod tests { let path = file.path().to_path_buf(); let writer = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap(); - writer.borrow_mut().write_line_bytes(b"a\xE9").unwrap(); + writer + .borrow_mut() + .write_line_bytes(b"a\xE9", true) + .unwrap(); writer.borrow_mut().flush().unwrap(); assert_eq!(fs::read(path).unwrap(), b"a\xE9\n"); } + + #[test] + fn test_write_line_bytes_appends_no_newline() { + 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\xE9", false) + .unwrap(); + writer.borrow_mut().flush().unwrap(); + + assert_eq!(fs::read(path).unwrap(), b"a\xE9"); + } } diff --git a/src/sed/processor.rs b/src/sed/processor.rs index 6549b9f2..2aa12317 100644 --- a/src/sed/processor.rs +++ b/src/sed/processor.rs @@ -397,7 +397,9 @@ fn substitute( // Write to file if needed. if let Some(ref writer) = sub.write_file { - writer.borrow_mut().write_line_bytes(pattern.as_bytes())?; + writer + .borrow_mut() + .write_line_bytes(pattern.as_bytes(), pattern.is_newline_terminated())?; } context.substitution_made = true; } @@ -861,7 +863,24 @@ 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())?; + writer + .borrow_mut() + .write_line_bytes(pattern.as_bytes(), pattern.is_newline_terminated())?; + } + 'W' => { + // 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(), + )?; } 'x' => { // Exchange the contents of the pattern and hold spaces. diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index 01fbbd78..aab7142e 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -1646,7 +1646,7 @@ check_output!( ); //////////////////////////////////////////////////////////// -// r, w commands +// r, w, W commands check_output!(read_ok, [format!("4r {LINES2}"), LINES1.to_string()]); check_output!(read_missing, ["5r /xyzzyxyzy42", LINES1]); check_output!(read_empty, ["6r input/empty", LINES1]); @@ -1701,6 +1701,23 @@ fn sandbox_rejects_write_command() -> std::io::Result<()> { Ok(()) } +#[test] +fn sandbox_rejects_first_line_write_command() -> std::io::Result<()> { + let temp = NamedTempFile::new()?; + let cmd = format!("W {}", temp.path().display()); + + new_ucmd!() + .args(&["--sandbox", &cmd, LINES1]) + .fails() + .stderr_contains("command not allowed with --sandbox"); + + let mut actual = String::new(); + temp.reopen()?.read_to_string(&mut actual)?; + assert!(actual.is_empty()); + + Ok(()) +} + #[test] fn write_single_file() -> std::io::Result<()> { let temp = NamedTempFile::new()?; @@ -1717,6 +1734,23 @@ fn write_single_file() -> std::io::Result<()> { Ok(()) } +#[test] +fn write_single_file_no_newline() -> std::io::Result<()> { + let temp = NamedTempFile::new()?; + let cmd = format!("w {}", temp.path().display()); + + new_ucmd!() + .args(&[cmd.as_str(), "input/no-new-line.txt"]) + .succeeds(); + + let mut actual = String::new(); + temp.reopen()?.read_to_string(&mut actual)?; + + assert_eq!(actual, "Hello", "Output did not match expected"); + + Ok(()) +} + #[test] fn write_two_files() -> std::io::Result<()> { let temp1 = NamedTempFile::new()?; @@ -1744,6 +1778,49 @@ fn write_two_files() -> std::io::Result<()> { Ok(()) } +#[test] +fn write_first_line_newline() -> std::io::Result<()> { + let temp = NamedTempFile::new()?; + let cmd = format!("N;W {}", temp.path().display()); + + new_ucmd!() + .args(&["-n", "-e", &cmd]) + .pipe_in("abc\ndef\n") + .succeeds(); + + let mut actual = String::new(); + temp.reopen()?.read_to_string(&mut actual)?; + assert_eq!(actual, "abc\n"); + + Ok(()) +} + +#[test] +fn write_first_line_no_newline() -> std::io::Result<()> { + let temp = NamedTempFile::new()?; + let cmd = format!("W {}", temp.path().display()); + + new_ucmd!() + .args(&["-n", "-e", &cmd]) + .pipe_in("abc") + .succeeds(); + + let mut actual = String::new(); + temp.reopen()?.read_to_string(&mut actual)?; + assert_eq!(actual, "abc"); + + Ok(()) +} + +#[test] +fn write_first_line_with_w_command_is_non_posix() { + new_ucmd!() + .args(&["--posix", "W /tmp/out"]) + .fails() + .code_is(1) + .stderr_is("sed: