From a634674624f2ee5d4ef6daa22ab967687f3f994f Mon Sep 17 00:00:00 2001 From: Anthony DePasquale Date: Fri, 2 Oct 2026 19:18:20 +0200 Subject: [PATCH 1/2] sed: continue with the next command after n instead of starting a new cycle --- src/sed/command.rs | 5 +++-- src/sed/processor.rs | 29 ++++++++++++++++++++--------- tests/by-util/test_sed.rs | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/sed/command.rs b/src/sed/command.rs index fbf335ee..c9d60842 100644 --- a/src/sed/command.rs +++ b/src/sed/command.rs @@ -392,8 +392,9 @@ pub enum SpaceFlag { pub struct InputAction { /// Next command to execute (rather than commands from start) pub next_command: Option>>, - /// Data to prepend to the read contents - pub prepend: Vec, + /// Data to prepend to the read contents (`N`), or `None` to replace + /// the pattern space with them (`n`) + pub prepend: Option>, } #[cfg(test)] diff --git a/src/sed/processor.rs b/src/sed/processor.rs index 3f0ebb8b..17f65950 100644 --- a/src/sed/processor.rs +++ b/src/sed/processor.rs @@ -654,12 +654,13 @@ fn process_file( // Set the script command from which to start. let mut current: Option>> = 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.extend_from_slice(pattern.as_bytes()); + // Continue processing the `N` or `n` command. + if let Some(mut combined_lines) = action.prepend { + combined_lines.push(b'\n'); + combined_lines.extend_from_slice(pattern.as_bytes()); - pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated()); + pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated()); + } action.next_command } else { // Start from the script top. @@ -779,7 +780,17 @@ fn process_file( list(output, &pattern, width, &command.location, context)?; } 'n' => { - break; + // Print the pattern space and continue with the next + // command once the next line is read, as `N` does. + if !context.quiet { + write_chunk(output, context, &pattern)?; + } + flush_appends(output, context)?; + context.input_action = Some(InputAction { + next_command: command.next.clone(), + prepend: None, + }); + continue 'lines; } 'N' => { flush_appends(output, context)?; @@ -789,7 +800,7 @@ fn process_file( // to perform when the next line is read. context.input_action = Some(InputAction { next_command: command.next.clone(), - prepend: pattern.as_bytes().to_vec(), + prepend: Some(pattern.as_bytes().to_vec()), }); continue 'lines; } @@ -943,9 +954,9 @@ fn process_file( // Take it even with -n, so that it does not carry over to the next file. if context.separate && let Some(action) = context.input_action.take() + && let Some(mut pending) = action.prepend && !context.quiet { - let mut pending = action.prepend; pending.push(b'\n'); output.write_bytes(&pending)?; if context.unbuffered { @@ -1005,8 +1016,8 @@ pub fn process_all_files( && !context.separate && !context.quiet && let Some(action) = context.input_action.take() + && let Some(mut pending) = action.prepend { - let mut pending = action.prepend; pending.push(b'\n'); output.write_bytes(&pending)?; } diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index add24b83..a91c18f6 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -1376,6 +1376,40 @@ check_output!(pattern_quit_2, [r"5q", LINES1, LINES2]); check_output!(pattern_re_reuse, ["-n", r"/_1/p;//p", LINES1]); check_output!(pattern_subst_re_reuse, ["-n", r"/_1/p;s//-N/p", LINES1]); +#[test] +fn test_next_continues_with_following_command() { + new_ucmd!() + .args(&["n;d"]) + .pipe_in("1\n2\n3\n4\n") + .succeeds() + .stdout_is("1\n3\n"); + new_ucmd!() + .args(&["-n", "-e", "a A", "-e", "n;p"]) + .pipe_in("a\nb\n") + .succeeds() + .stdout_is("A\nb\n"); + // At the end of input, `n` ends processing. + new_ucmd!() + .args(&["n;q5"]) + .pipe_in("1\n") + .succeeds() + .stdout_is("1\n"); +} + +#[test] +fn test_next_reads_across_files_unless_separate() { + new_ucmd!() + .args(&["n;s/^/X/", "-", "input/two-lines.txt"]) + .pipe_in("a\n") + .succeeds() + .stdout_is("a\nXline one\nline two\n"); + new_ucmd!() + .args(&["-s", "n;s/^/X/", "-", "input/two-lines.txt"]) + .pipe_in("a\n") + .succeeds() + .stdout_is("a\nline one\nXline two\n"); +} + #[test] fn test_quit_exit_code() { new_ucmd!() From e7920474d6731a1a69d78ff8e5f4b1d0bd810b9a Mon Sep 17 00:00:00 2001 From: Anthony DePasquale Date: Sat, 3 Oct 2026 17:20:42 +0200 Subject: [PATCH 2/2] sed: name the two ways n and N read the next line, and test n with -i --- src/sed/command.rs | 14 +++++++++++--- src/sed/processor.rs | 18 +++++++++--------- tests/by-util/test_sed.rs | 8 ++++++++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/sed/command.rs b/src/sed/command.rs index c9d60842..1af1e630 100644 --- a/src/sed/command.rs +++ b/src/sed/command.rs @@ -392,9 +392,17 @@ pub enum SpaceFlag { pub struct InputAction { /// Next command to execute (rather than commands from start) pub next_command: Option>>, - /// Data to prepend to the read contents (`N`), or `None` to replace - /// the pattern space with them (`n`) - pub prepend: Option>, + /// How the read line becomes the pattern space + pub next_line: NextLine, +} + +#[derive(Debug, Clone)] +/// How a line read by `n` or `N` becomes the pattern space +pub enum NextLine { + /// Replace the pattern space with the line (`n`) + Replace, + /// Append the line, after a newline, to these contents (`N`) + Append(Vec), } #[cfg(test)] diff --git a/src/sed/processor.rs b/src/sed/processor.rs index 17f65950..298a974e 100644 --- a/src/sed/processor.rs +++ b/src/sed/processor.rs @@ -9,8 +9,8 @@ // file that was distributed with this source code. use crate::sed::command::{ - Address, AppendElement, CharacterMode, Command, CommandData, InputAction, ProcessingContext, - Transliteration, + Address, AppendElement, CharacterMode, Command, CommandData, InputAction, NextLine, + ProcessingContext, Transliteration, }; use crate::sed::delimited_parser::os_string_from_bytes; use crate::sed::error_handling::{ScriptLocation, input_runtime_error}; @@ -655,7 +655,7 @@ fn process_file( let mut current: Option>> = if let Some(action) = context.input_action.take() { // Continue processing the `N` or `n` command. - if let Some(mut combined_lines) = action.prepend { + if let NextLine::Append(mut combined_lines) = action.next_line { combined_lines.push(b'\n'); combined_lines.extend_from_slice(pattern.as_bytes()); @@ -788,7 +788,7 @@ fn process_file( flush_appends(output, context)?; context.input_action = Some(InputAction { next_command: command.next.clone(), - prepend: None, + next_line: NextLine::Replace, }); continue 'lines; } @@ -800,7 +800,7 @@ fn process_file( // to perform when the next line is read. context.input_action = Some(InputAction { next_command: command.next.clone(), - prepend: Some(pattern.as_bytes().to_vec()), + next_line: NextLine::Append(pattern.as_bytes().to_vec()), }); continue 'lines; } @@ -950,11 +950,11 @@ fn process_file( } } - // Handle any N command remains. - // Take it even with -n, so that it does not carry over to the next file. + // Handle any n or N command remains. + // Take them even with -n, so that they do not carry over to the next file. if context.separate && let Some(action) = context.input_action.take() - && let Some(mut pending) = action.prepend + && let NextLine::Append(mut pending) = action.next_line && !context.quiet { pending.push(b'\n'); @@ -1016,7 +1016,7 @@ pub fn process_all_files( && !context.separate && !context.quiet && let Some(action) = context.input_action.take() - && let Some(mut pending) = action.prepend + && let NextLine::Append(mut pending) = action.next_line { pending.push(b'\n'); output.write_bytes(&pending)?; diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index a91c18f6..5597d2fe 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -2578,6 +2578,14 @@ fn in_place_edit_separate() -> std::io::Result<()> { "a1+a2\na3\n", "b1+b2\nb3\n", ), + // n on the last line of a file does not read the first line of the next. + ( + &["-i", "-e", "n;s/^/X/"], + a, + b, + "a1\nXa2\na3\n", + "b1\nXb2\nb3\n", + ), // Lines collected in the hold space stay in their own file. ( &["-i", "-e", r"H;$!d;x;s/\n/,/g"],