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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/sed/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 31 additions & 7 deletions src/sed/named_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Comment on lines +58 to +64
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,
Expand Down Expand Up @@ -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");
}
}
23 changes: 21 additions & 2 deletions src/sed/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.
Expand Down
79 changes: 78 additions & 1 deletion tests/by-util/test_sed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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()?;
Expand All @@ -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()?;
Expand Down Expand Up @@ -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: <script argument 1>:1:1: error: invalid command code `W'\n");
}

Comment thread
dspinellis marked this conversation as resolved.
////////////////////////////////////////////////////////////
// =, l, F commands
check_output!(number_continuous, ["/l2_/=", LINES1, LINES2]);
Expand Down
Loading