diff --git a/src/sed/command.rs b/src/sed/command.rs index 250bcbc..ddb8f65 100644 --- a/src/sed/command.rs +++ b/src/sed/command.rs @@ -202,6 +202,7 @@ pub struct Substitution { pub occurrence: usize, // Which occurrence to substitute pub print_flag: bool, // True if 'p' flag pub ignore_case: bool, // True if 'I' flag + pub multiline: bool, // True if 'm' or 'M' flag pub write_file: Option>>, // Writer to file if 'w' flag is used } diff --git a/src/sed/compiler.rs b/src/sed/compiler.rs index bcfff31..ef8f6af 100644 --- a/src/sed/compiler.rs +++ b/src/sed/compiler.rs @@ -460,7 +460,7 @@ fn compile_address( } Ok(Address::Re(compile_regex( - lines, line, &re, context, icase, + lines, line, &re, context, icase, false, )?)) } '$' => { @@ -628,6 +628,7 @@ fn compile_regex( pattern: &str, context: &ProcessingContext, icase: bool, + multiline: bool, ) -> UResult> { if pattern.is_empty() { return Ok(None); @@ -640,11 +641,17 @@ fn compile_regex( &bre_to_ere(pattern) }; - // Add case-insensitive modifier if needed. - let pattern = if icase { - format!("(?i){pattern}") - } else { + let mut modifiers = String::new(); + if icase { + modifiers.push('i'); + } + if multiline { + modifiers.push('m'); + } + let pattern = if modifiers.is_empty() { pattern.to_string() + } else { + format!("(?{modifiers}){pattern}") }; // Compile into engine. @@ -793,7 +800,7 @@ fn compile_subst_command( subst.replacement = compile_replacement(lines, line)?; compile_subst_flags(lines, line, &mut subst)?; - if pattern.is_empty() && subst.ignore_case { + if pattern.is_empty() && (subst.ignore_case || subst.multiline) { return compilation_error( lines, line, @@ -801,8 +808,15 @@ fn compile_subst_command( ); } - // Compile regex with now known ignore_case flag. - subst.regex = compile_regex(lines, line, &pattern, context, subst.ignore_case)?; + // Compile regex with now known modifier flags. + subst.regex = compile_regex( + lines, + line, + &pattern, + context, + subst.ignore_case, + subst.multiline, + )?; // Catch invalid group references at compile time, if possible. if let Some(regex) = &subst.regex @@ -871,6 +885,7 @@ pub fn compile_subst_flags( subst.occurrence = 1; // default subst.print_flag = false; subst.ignore_case = false; + subst.multiline = false; subst.write_file = None; loop { @@ -903,6 +918,11 @@ pub fn compile_subst_flags( line.advance(); } + 'm' | 'M' => { + subst.multiline = true; + line.advance(); + } + _c @ '1'..='9' => { if seen_g_or_n { return compilation_error( @@ -1562,7 +1582,7 @@ mod tests { #[test] fn test_compile_re_basic() { let (lines, chars) = dummy_providers(); - let regex = compile_regex(&lines, &chars, "abc", &ctx(), false) + let regex = compile_regex(&lines, &chars, "abc", &ctx(), false, false) .unwrap() .expect("regex should be present"); assert!(regex.is_match(&mut IOChunk::new_from_str("abc")).unwrap()); @@ -1572,7 +1592,7 @@ mod tests { #[test] fn test_compile_re_case_insensitive() { let (lines, chars) = dummy_providers(); - let regex = compile_regex(&lines, &chars, "abc", &ctx(), true) + let regex = compile_regex(&lines, &chars, "abc", &ctx(), true, false) .unwrap() .expect("regex should be present"); assert!(regex.is_match(&mut IOChunk::new_from_str("abc")).unwrap()); @@ -1583,10 +1603,23 @@ mod tests { #[test] fn test_compile_re_invalid() { let (lines, chars) = dummy_providers(); - let result = compile_regex(&lines, &chars, "a[d", &ctx(), false); + let result = compile_regex(&lines, &chars, "a[d", &ctx(), false, false); assert!(result.is_err()); // Should fail due to open bracketed expression } + #[test] + fn test_compile_re_multiline() { + let (lines, chars) = dummy_providers(); + let regex = compile_regex(&lines, &chars, "^bar", &ctx(), false, true) + .unwrap() + .expect("regex should be present"); + assert!( + regex + .is_match(&mut IOChunk::new_from_str("foo\nbar")) + .unwrap() + ); + } + // compile_address #[test] fn test_compile_addr_line_number() { @@ -2064,6 +2097,24 @@ mod tests { assert!(subst.ignore_case); } + #[test] + fn test_compile_subst_flag_uppercase_m() { + let (lines, mut chars) = make_providers("M"); + let mut subst = Substitution::default(); + + compile_subst_flags(&lines, &mut chars, &mut subst).unwrap(); + assert!(subst.multiline); + } + + #[test] + fn test_compile_subst_flag_m_lowercase() { + let (lines, mut chars) = make_providers("m"); + let mut subst = Substitution::default(); + + compile_subst_flags(&lines, &mut chars, &mut subst).unwrap(); + assert!(subst.multiline); + } + #[test] fn test_compile_subst_flag_number() { let (lines, mut chars) = make_providers("3"); diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index b31fed7..5f2870f 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -318,6 +318,24 @@ check_output!(subst_re_reuse, ["-e", r"2s//M/;1s/l/L/", LINES1]); check_output!(subst_newline_class, ["-n", r"1{;N;s/[\n]/X/;p;}", LINES1]); check_output!(subst_newline_re, ["-n", r"1{;N;s/\n/X/;p;}", LINES1]); +#[test] +fn subst_multiline_flag_matches_embedded_line_start() { + new_ucmd!() + .arg("N;s/^./X/gm") + .pipe_in("foo\nbar\n") + .succeeds() + .stdout_is("Xoo\nXar\n"); +} + +#[test] +fn subst_multiline_flag_matches_embedded_line_end() { + new_ucmd!() + .arg("N;s/.$/X/gM") + .pipe_in("foo\nbar\n") + .succeeds() + .stdout_is("foX\nbaX\n"); +} + // Check appropriate selection and behavior of fast_Regex matcher // Literal matcher check_output!(subst_literal_start, ["-e", r"s/^l1/L1/", LINES1]);