From d1a8efd5630aab465493b8345e2cd7e586d1c81e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 19 Oct 2026 17:44:30 +0200 Subject: [PATCH] sed: accept a number combined with the `g` substitute flag `s/x/y/2g` (replace the 2nd match and every later one) and `s/x/y/g2` are valid in GNU sed, but were rejected with "multiple 'g' or numeric flags". Track `g` separately from the occurrence number so the two can be combined; repeating either one is still an error. --- src/sed/command.rs | 1 + src/sed/compiler.rs | 35 ++++++++++++++++++++++++----------- src/sed/processor.rs | 12 ++++++------ tests/by-util/test_sed.rs | 16 ++++++++++++++++ 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/sed/command.rs b/src/sed/command.rs index 7083ba3c..3b28eaf6 100644 --- a/src/sed/command.rs +++ b/src/sed/command.rs @@ -229,6 +229,7 @@ pub struct Substitution { pub regex: Option, // Regular expression pub replacement: ReplacementTemplate, // Specified broken-down replacement pub occurrence: usize, // Which occurrence to substitute + pub global: bool, // True if 'g' flag: also every later one pub print_flag: bool, // True if 'p' flag pub p_before_e: bool, // True if 'p' appears before 'e' pub ignore_case: bool, // True if 'I' flag diff --git a/src/sed/compiler.rs b/src/sed/compiler.rs index 65b6c99c..3960cda1 100644 --- a/src/sed/compiler.rs +++ b/src/sed/compiler.rs @@ -958,9 +958,10 @@ pub fn compile_subst_flags( posix: bool, sandbox: bool, ) -> UResult<()> { - let mut seen_g_or_n = false; + let mut seen_number = false; subst.occurrence = 1; // default + subst.global = false; subst.print_flag = false; subst.p_before_e = false; subst.ignore_case = false; @@ -976,15 +977,14 @@ pub fn compile_subst_flags( match line.current() { 'g' => { - if seen_g_or_n { + if subst.global { return compilation_error( lines, line, "multiple 'g' or numeric flags in substitute command", ); } - seen_g_or_n = true; - subst.occurrence = 0; + subst.global = true; line.advance(); } @@ -1024,7 +1024,7 @@ pub fn compile_subst_flags( } _c @ '1'..='9' => { - if seen_g_or_n { + if seen_number { return compilation_error( lines, line, @@ -1049,7 +1049,7 @@ pub fn compile_subst_flags( } subst.occurrence = number; - seen_g_or_n = true; + seen_number = true; } 'w' => { @@ -2601,7 +2601,8 @@ mod tests { let mut subst = Substitution::default(); compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap(); - assert_eq!(subst.occurrence, 0); // 'g' means all occurrences + assert_eq!(subst.occurrence, 1); + assert!(subst.global); } #[test] @@ -2659,8 +2660,20 @@ mod tests { } #[test] - fn test_compile_subst_flag_g_and_number_should_fail() { - let (lines, mut chars) = make_providers("g3"); + fn test_compile_subst_flag_g_and_number() { + for flags in ["g3", "3g"] { + let (lines, mut chars) = make_providers(flags); + let mut subst = Substitution::default(); + + compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap(); + assert_eq!(subst.occurrence, 3); + assert!(subst.global); + } + } + + #[test] + fn test_compile_subst_flag_multiple_g_should_fail() { + let (lines, mut chars) = make_providers("gpg"); let mut subst = Substitution::default(); let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap_err(); @@ -2671,8 +2684,8 @@ mod tests { } #[test] - fn test_compile_subst_flag_number_and_g_should_fail() { - let (lines, mut chars) = make_providers("2g"); + fn test_compile_subst_flag_multiple_numbers_should_fail() { + let (lines, mut chars) = make_providers("2g3"); let mut subst = Substitution::default(); let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap_err(); diff --git a/src/sed/processor.rs b/src/sed/processor.rs index 9cc98c1f..d7fe49b2 100644 --- a/src/sed/processor.rs +++ b/src/sed/processor.rs @@ -305,8 +305,8 @@ fn substitute( // The following let block allows a common input_runtime_error to be // called once in all cases, and most importantly, to finish the regex // mutable borrowing of context, so as to reuse context in the error call. - let subst_result = match (sub.occurrence, sub.replacement.max_group_number) { - (1, 0) => { + let subst_result = match (sub.occurrence, sub.global, sub.replacement.max_group_number) { + (1, false, 0) => { // Example: s/foo/bar/: find() is enough. match regex.find(pattern) { Err(e) => Err(e), @@ -323,7 +323,7 @@ fn substitute( } } - (1, _) => { + (1, false, _) => { // Example: s/\(.\)\(.\)/\2\1/: captures() is enough. match regex.captures(pattern) { Err(e) => Err(e), @@ -341,7 +341,7 @@ fn substitute( } } - (_, _) => { + (_, _, _) => { // Example: s/(.)(.)/\2\1/3: captures_iter() is needed. // Iterate over multiple captures of the RE in the pattern. 'captures: { @@ -357,7 +357,7 @@ fn substitute( // Always write the unmatched text before this match. result.extend_from_slice(&text[last_end..m.start()]); - if sub.occurrence == 0 || count == sub.occurrence { + if count == sub.occurrence || (sub.global && count > sub.occurrence) { let replacement = sub.replacement.apply_captures(command, &caps)?; result.extend_from_slice(&replacement); replaced = true; @@ -370,7 +370,7 @@ fn substitute( // Early exit if only a specific occurrence, // (likely 1) needed replacing. - if count == sub.occurrence { + if !sub.global && count == sub.occurrence { break 'captures Ok(()); } } diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index 83d96035..9a57ba49 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -579,6 +579,22 @@ fn subst_multiline_flag_matches_embedded_line_end() { .stdout_is("foX\nbaX\n"); } +#[test] +fn subst_number_and_g_replace_from_that_match_on() { + for flags in ["2g", "g2"] { + new_ucmd!() + .arg(format!("s/a/b/{flags}")) + .pipe_in("aaaa\n") + .succeeds() + .stdout_is("abbb\n"); + } + new_ucmd!() + .arg(r"s/\(a\)/<\1>/3g") + .pipe_in("aaaa\n") + .succeeds() + .stdout_is("aa\n"); +} + // Check appropriate selection and behavior of fast_Regex matcher // Literal matcher check_output!(subst_literal_start, ["-e", r"s/^l1/L1/", LINES1]);