From 35ca179e239555f9d3136d728226c4f419ce1e11 Mon Sep 17 00:00:00 2001
From: sap1110 <238814652+sap1110@users.noreply.github.com>
Date: Mon, 28 Sep 2026 11:12:37 +0530
Subject: [PATCH 1/2] sed: start a range whose numeric first address was
already passed
When a range command is first evaluated on a line after its numeric first
address (inside a block, or after d, n, N, c or a branch skipped it), start
the range on the current line, as GNU sed does, unless a numeric second
address has also been passed. The range can start this way only once.
Fixes #541
---
src/sed/command.rs | 2 ++
src/sed/processor.rs | 25 +++++++++++++-------
tests/by-util/test_sed.rs | 49 +++++++++++++++++++++++++++++++++++++++
3 files changed, 68 insertions(+), 8 deletions(-)
diff --git a/src/sed/command.rs b/src/sed/command.rs
index fbf335ee..73f02068 100644
--- a/src/sed/command.rs
+++ b/src/sed/command.rs
@@ -334,6 +334,7 @@ pub struct Command {
pub addr2: Option
, // End address
pub non_select: bool, // True if '!'
pub start_line: Option, // Start line number (or None if unlatched)
+ pub range_started: bool, // True once the range has been entered
pub data: CommandData, // Command-specific data
pub next: Option>>, // Pointer to next command
pub location: ScriptLocation, // Command's definition location
@@ -347,6 +348,7 @@ impl Default for Command {
addr2: None,
non_select: false,
start_line: None,
+ range_started: false,
data: CommandData::None,
next: None,
location: ScriptLocation::default(),
diff --git a/src/sed/processor.rs b/src/sed/processor.rs
index 3f0ebb8b..2ed1b0ea 100644
--- a/src/sed/processor.rs
+++ b/src/sed/processor.rs
@@ -124,7 +124,19 @@ fn applies(
}
} else if let Some(addr1) = &command.addr1 {
// See if latch must start.
- if match_address(addr1, reader, pattern, context, &command.location)? {
+ let starts = match addr1 {
+ // A numeric first address may already have been passed
+ // when the command is first evaluated (e.g. inside a block,
+ // or after n, N, d or a branch skipped it). The range then
+ // starts on the current line, unless a numeric second
+ // address has also been passed. This happens only once.
+ Address::Line(n) if linenum > *n => {
+ !command.range_started && !matches!(addr2, Address::Line(m) if *m < linenum)
+ }
+ _ => match_address(addr1, reader, pattern, context, &command.location)?,
+ };
+ if starts {
+ command.range_started = true;
match addr2 {
Address::Line(n) if linenum >= *n => {
context.last_address = true;
@@ -961,13 +973,10 @@ fn reset_latched_address_ranges(range_commands: &mut [Rc>]) {
for cmd_rc in range_commands.iter() {
let mut cmd = cmd_rc.borrow_mut();
- cmd.start_line =
- // Check for address-spec line 0 pre-latch extension.
- if let Some(addr1) = &cmd.addr1 && matches!(addr1, Address::Line(0)) {
- Some(0)
- } else {
- None
- };
+ // Check for address-spec line 0 pre-latch extension.
+ let pre_latched = matches!(cmd.addr1, Some(Address::Line(0)));
+ cmd.start_line = if pre_latched { Some(0) } else { None };
+ cmd.range_started = pre_latched;
}
}
diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs
index ace171d3..edf0f009 100644
--- a/tests/by-util/test_sed.rs
+++ b/tests/by-util/test_sed.rs
@@ -2760,6 +2760,55 @@ fn test_step_match_zero_closes_range() {
.stdout_is("1\n2\nX\n4\n5\n");
}
+#[test]
+fn test_range_numeric_start_already_passed() {
+ // The inner range is first evaluated on line 3, after its start line.
+ for (script, expected) in [
+ ("3,6{ 1,4p }", "3\n4\n"),
+ ("3,6{ 3,4p }", "3\n4\n"),
+ ("3,6{ 1,3p }", "3\n"),
+ ("3,6{ 1,2p }", ""),
+ ("3,6{ 1,~4p }", "3\n4\n"),
+ ("3,6{ 1,+1p }", "3\n4\n"),
+ ("3,6{ 1,/5/p }", "3\n4\n5\n"),
+ ("3,6{ 1,$p }", "3\n4\n5\n6\n"),
+ ("3,6{ 1,2!p }", "3\n4\n5\n6\n"),
+ ] {
+ new_ucmd!()
+ .args(&["-n", script])
+ .pipe_in("1\n2\n3\n4\n5\n6\n7\n")
+ .succeeds()
+ .stdout_is(expected);
+ }
+}
+
+#[test]
+fn test_range_numeric_start_passed_starts_once() {
+ // Once the range has ended, it does not start again.
+ for (script, expected) in [
+ ("/[2468]/{ 1,/[48]/p }", "2\n4\n"),
+ ("/[3579]/{ 2,/./p }", "3\n5\n"),
+ ("1,/3/p", "1\n2\n3\n"),
+ ("0,/2/p", "1\n2\n"),
+ ("/[3-9]/{ 0,/5/p }", "3\n4\n5\n"),
+ ] {
+ new_ucmd!()
+ .args(&["-n", script])
+ .pipe_in("1\n2\n3\n4\n5\n6\n7\n8\n9\n")
+ .succeeds()
+ .stdout_is(expected);
+ }
+}
+
+#[test]
+fn test_range_numeric_start_skipped_by_d() {
+ new_ucmd!()
+ .args(&["-e", "1,3d", "-e", "2,5s/$/!/"])
+ .pipe_in("a\nb\nc\nd\ne\nf\n")
+ .succeeds()
+ .stdout_is("d!\ne!\nf\n");
+}
+
#[test]
fn test_step_non_numeric() {
for (script, column) in [("1~/x/p", 6), ("1,~/x/p", 7)] {
From 6efcc062e0bb4b605cf26cf461d7b35cb52ecbe8 Mon Sep 17 00:00:00 2001
From: sap1110 <238814652+sap1110@users.noreply.github.com>
Date: Tue, 29 Sep 2026 17:28:33 +0530
Subject: [PATCH 2/2] fixup! sed: start a range whose numeric first address was
already passed
---
src/sed/processor.rs | 6 +-----
tests/by-util/test_sed.rs | 19 +++++++++++++++++++
2 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/src/sed/processor.rs b/src/sed/processor.rs
index 2ed1b0ea..5aa8ab57 100644
--- a/src/sed/processor.rs
+++ b/src/sed/processor.rs
@@ -125,11 +125,7 @@ fn applies(
} else if let Some(addr1) = &command.addr1 {
// See if latch must start.
let starts = match addr1 {
- // A numeric first address may already have been passed
- // when the command is first evaluated (e.g. inside a block,
- // or after n, N, d or a branch skipped it). The range then
- // starts on the current line, unless a numeric second
- // address has also been passed. This happens only once.
+ // Numeric start already passed (block, d, n, branch): start once here.
Address::Line(n) if linenum > *n => {
!command.range_started && !matches!(addr2, Address::Line(m) if *m < linenum)
}
diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs
index edf0f009..bedf2671 100644
--- a/tests/by-util/test_sed.rs
+++ b/tests/by-util/test_sed.rs
@@ -2800,6 +2800,25 @@ fn test_range_numeric_start_passed_starts_once() {
}
}
+#[test]
+fn test_range_numeric_start_passed_separate_files() -> std::io::Result<()> {
+ // With -s the "started once" state is reset, so the range starts again in the second file.
+ let input = "1\n2\n3\n4\n5\n6\n7\n";
+ let (_dir, path1, path2) = two_inputs(input, input)?;
+
+ new_ucmd!()
+ .args(&[
+ "-n",
+ "-s",
+ "3,6{ 1,4p }",
+ path1.to_str().unwrap(),
+ path2.to_str().unwrap(),
+ ])
+ .succeeds()
+ .stdout_is("3\n4\n3\n4\n");
+ Ok(())
+}
+
#[test]
fn test_range_numeric_start_skipped_by_d() {
new_ucmd!()