From 99e302e0cd09d136cbc664b633addec8a358899f Mon Sep 17 00:00:00 2001 From: LoukasPap Date: Sat, 5 Sep 2026 15:50:48 +0300 Subject: [PATCH 1/4] Fix --posix to disable the GNU regular expression escapes - Add a POSIX parameter to `parse_regex_for_mode()` that emits the literal character for `\?`, `\+`, `\|`, `\w`, `\W`, `\s`, `\S`, `\b`, `\B`, `\<`, `\>`, `` \` `` and `\'` - Emit the character bare in BRE, where `bre_to_ere()` escapes it afterwards, and keep the backslash on `\?`, `\+` and `\|` in ERE - Add unit and integration tests --- src/sed/compiler.rs | 16 +++++++- src/sed/delimited_parser.rs | 78 ++++++++++++++++++++++++++++++++++++- tests/by-util/test_sed.rs | 67 +++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/sed/compiler.rs b/src/sed/compiler.rs index 65b6c99c..304d3a69 100644 --- a/src/sed/compiler.rs +++ b/src/sed/compiler.rs @@ -478,7 +478,13 @@ fn compile_address( } else { RegexMode::Basic }; - let re = parse_regex_for_mode(lines, line, regex_mode, context.character_mode)?; + let re = parse_regex_for_mode( + lines, + line, + regex_mode, + context.character_mode, + context.posix, + )?; // Skip over delimiter line.advance(); @@ -861,7 +867,13 @@ fn compile_subst_command( } else { RegexMode::Basic }; - let pattern = parse_regex_for_mode(lines, line, regex_mode, context.character_mode)?; + let pattern = parse_regex_for_mode( + lines, + line, + regex_mode, + context.character_mode, + context.posix, + )?; let mut subst = Box::new(Substitution::default()); subst.replacement = compile_replacement(lines, line, context.character_mode)?; diff --git a/src/sed/delimited_parser.rs b/src/sed/delimited_parser.rs index b6a9b2d0..2e9b1db3 100644 --- a/src/sed/delimited_parser.rs +++ b/src/sed/delimited_parser.rs @@ -359,15 +359,27 @@ pub fn parse_regex( line: &mut ScriptCharProvider, regex_mode: RegexMode, ) -> UResult> { - parse_regex_for_mode(lines, line, regex_mode, CharacterMode::Utf8) + parse_regex_for_mode(lines, line, regex_mode, CharacterMode::Utf8, false) +} + +/// Return true if a backslash followed by `c` is a GNU regular expression +/// extension, which under --posix stands for the literal character `c`. +fn is_gnu_regex_escape(c: char) -> bool { + matches!( + c, + '?' | '+' | '|' | 'w' | 'W' | 's' | 'S' | 'b' | 'B' | '<' | '>' | '`' | '\'' + ) } /// Parse a regular expression according to the current character mode. +/// Under `posix` the GNU regular expression extensions are disabled, with each +/// one standing for the literal character that follows its backslash. pub fn parse_regex_for_mode( lines: &ScriptLineProvider, line: &mut ScriptCharProvider, regex_mode: RegexMode, character_mode: CharacterMode, + posix: bool, ) -> UResult> { let delimiter = scan_delimiter(lines, line)?; let mut result = Vec::new(); @@ -389,6 +401,17 @@ pub fn parse_regex_for_mode( line.advance(); continue; } + if posix && is_gnu_regex_escape(line.current()) { + // ? + | must keep their backslash to remain literals. + if matches!(regex_mode, RegexMode::Extended) + && matches!(line.current(), '?' | '+' | '|') + { + result.push(b'\\'); + } + result.push(line.current_byte()); + line.advance(); + continue; + } if line.current() == '{' && matches!(regex_mode, RegexMode::Basic) { validate_quantifier_structure(lines, line, delimiter, RegexMode::Basic)?; let quantifier = validate_quantifier_numbers(lines, line)?; @@ -1254,6 +1277,59 @@ mod tests { assert_eq!(line.current(), '/'); } + // parse_regex_for_mode: GNU extensions under --posix + #[test] + fn test_posix_bre_emits_bare_characters() { + // bre_to_ere() escapes the ERE metacharacters, so they are left bare. + let (lines, mut line) = make_providers(r"/0\?a\+b\|/"); + let parsed = parse_regex_for_mode( + &lines, + &mut line, + RegexMode::Basic, + CharacterMode::Utf8, + true, + ) + .unwrap(); + assert_eq!(parsed, b"0?a+b|"); + + let (lines, mut line) = make_providers(r"/\w\s\b\<\`/"); + let parsed = parse_regex_for_mode( + &lines, + &mut line, + RegexMode::Basic, + CharacterMode::Utf8, + true, + ) + .unwrap(); + assert_eq!(parsed, b"wsb<`"); + } + + #[test] + fn test_posix_ere_keeps_metacharacters_escaped() { + // In EREs \? \+ \| are already literals, so the backslash stays. + let (lines, mut line) = make_providers(r"/a\?b\+c\|/"); + let parsed = parse_regex_for_mode( + &lines, + &mut line, + RegexMode::Extended, + CharacterMode::Utf8, + true, + ) + .unwrap(); + assert_eq!(parsed, br"a\?b\+c\|"); + + let (lines, mut line) = make_providers(r"/\w\s\b/"); + let parsed = parse_regex_for_mode( + &lines, + &mut line, + RegexMode::Extended, + CharacterMode::Utf8, + true, + ) + .unwrap(); + assert_eq!(parsed, b"wsb"); + } + // validate_quantifier_structure //BRE tests #[test] diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index 83d96035..4e2e5898 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -808,6 +808,73 @@ fn test_subst_e_flag_no_match_no_exec() { .stdout_is("hello\n"); } +//////////////////////////////////////////////////////////// +// GNU regular expression extensions and --posix +// Under --posix the GNU extensions \? \+ \| \w \W \s \S \b \B \< \> \` \' +// stand for the literal character following the backslash. + +#[test] +fn test_posix_disables_gnu_bre_operators() { + // The non-POSIX behavior is covered by the subst_quantifier_* and + // subst_alternation_operator tests above. + new_ucmd!() + .args(&["--posix", "-e", r"/0\?/d"]) + .pipe_in("a\nb\n0?\n") + .succeeds() + .stdout_is("a\nb\n"); + new_ucmd!() + .args(&["--posix", "-e", r"/a\+/d"]) + .pipe_in("a\nb\na+\n") + .succeeds() + .stdout_is("a\nb\n"); + new_ucmd!() + .args(&["--posix", "-e", r"/a\|b/d"]) + .pipe_in("a\nb\na|b\n") + .succeeds() + .stdout_is("a\nb\n"); +} + +#[test] +fn test_posix_disables_gnu_character_classes() { + new_ucmd!() + .args(&["--posix", "-e", r"/\w/d"]) + .pipe_in("a\nb\nw\n") + .succeeds() + .stdout_is("a\nb\n"); + new_ucmd!() + .args(&["-e", r"/\w/d"]) + .pipe_in("a\nb\nw\n") + .succeeds() + .stdout_is(""); +} + +#[test] +fn test_posix_disables_gnu_extensions_in_ere() { + new_ucmd!() + .args(&["--posix", "-E", "-e", r"/\w/d"]) + .pipe_in("aw\nx\n") + .succeeds() + .stdout_is("x\n"); + new_ucmd!() + .args(&["-E", "-e", r"/\w/d"]) + .pipe_in("aw\nx\n") + .succeeds() + .stdout_is(""); +} + +#[test] +fn test_posix_keeps_posix_regex_constructs() { + // Groups, quantifiers, operators and character escapes are POSIX, + // so --posix must leave them alone + for script in [r"s/\(a\)a/X/", r"s/a\{2\}/X/", r"s/a\x61/X/"] { + new_ucmd!() + .args(&["--posix", "-e", script]) + .pipe_in("aab\n") + .succeeds() + .stdout_is("Xb\n"); + } +} + //////////////////////////////////////////////////////////// // e command (execute) // The with-argument form writes the shell's raw, unmodified output to the From d26d8d597f0a2f2188e774dddec1cdd19af40c28 Mon Sep 17 00:00:00 2001 From: LoukasPap Date: Thu, 24 Sep 2026 14:40:50 +0300 Subject: [PATCH 2/4] Split test case to separate POSIX and GNU operators and skip branch for ? + | --- src/sed/delimited_parser.rs | 24 ++++++++++-------------- tests/by-util/test_sed.rs | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/sed/delimited_parser.rs b/src/sed/delimited_parser.rs index 2e9b1db3..ffc78593 100644 --- a/src/sed/delimited_parser.rs +++ b/src/sed/delimited_parser.rs @@ -362,13 +362,15 @@ pub fn parse_regex( parse_regex_for_mode(lines, line, regex_mode, CharacterMode::Utf8, false) } -/// Return true if a backslash followed by `c` is a GNU regular expression -/// extension, which under --posix stands for the literal character `c`. -fn is_gnu_regex_escape(c: char) -> bool { - matches!( - c, - '?' | '+' | '|' | 'w' | 'W' | 's' | 'S' | 'b' | 'B' | '<' | '>' | '`' | '\'' - ) +/// Return true if a backslash followed by `c` is a GNU ERE +/// that must be emitted as the literal character `c` under --posix. +/// In Extended mode `? + |` are excluded, because `\?` is already a literal there. +fn is_gnu_regex_escape(c: char, regex_mode: RegexMode) -> bool { + match c { + '?' | '+' | '|' => matches!(regex_mode, RegexMode::Basic), + 'w' | 'W' | 's' | 'S' | 'b' | 'B' | '<' | '>' | '`' | '\'' => true, + _ => false, + } } /// Parse a regular expression according to the current character mode. @@ -401,13 +403,7 @@ pub fn parse_regex_for_mode( line.advance(); continue; } - if posix && is_gnu_regex_escape(line.current()) { - // ? + | must keep their backslash to remain literals. - if matches!(regex_mode, RegexMode::Extended) - && matches!(line.current(), '?' | '+' | '|') - { - result.push(b'\\'); - } + if posix && is_gnu_regex_escape(line.current(), regex_mode) { result.push(line.current_byte()); line.advance(); continue; diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index 4e2e5898..b1136f68 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -864,9 +864,8 @@ fn test_posix_disables_gnu_extensions_in_ere() { #[test] fn test_posix_keeps_posix_regex_constructs() { - // Groups, quantifiers, operators and character escapes are POSIX, - // so --posix must leave them alone - for script in [r"s/\(a\)a/X/", r"s/a\{2\}/X/", r"s/a\x61/X/"] { + // Groups and quantifiers are POSIX, so --posix must leave them alone. + for script in [r"s/\(a\)a/X/", r"s/a\{2\}/X/"] { new_ucmd!() .args(&["--posix", "-e", script]) .pipe_in("aab\n") @@ -875,6 +874,18 @@ fn test_posix_keeps_posix_regex_constructs() { } } +#[test] +fn test_posix_keeps_gnu_character_escapes() { + // The character escapes are GNU extensions, but GNU sed keeps them under --posix. + for script in [r"s/\x61/X/", r"s/\o141/X/", r"s/\d097/X/"] { + new_ucmd!() + .args(&["--posix", "-e", script]) + .pipe_in("ab\n") + .succeeds() + .stdout_is("Xb\n"); + } +} + //////////////////////////////////////////////////////////// // e command (execute) // The with-argument form writes the shell's raw, unmodified output to the From a39e090813f28c7498040c1dfa3721563ce6820b Mon Sep 17 00:00:00 2001 From: LoukasPap Date: Thu, 24 Sep 2026 15:21:29 +0300 Subject: [PATCH 3/4] Fix backslash handling inside bracket expressions A backslash is only special inside a bracket expression when it starts a character escape. Elsewhere it is an ordinary member of the set and does not quote the character that follows, so `[\w]` matches a backslash or a w, and `[a\]bc]` ends at the first `]`. This matches GNU sed in both basic and extended mode. --- src/sed/delimited_parser.rs | 14 ++++++++----- tests/by-util/test_sed.rs | 40 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/sed/delimited_parser.rs b/src/sed/delimited_parser.rs index ffc78593..0072ce47 100644 --- a/src/sed/delimited_parser.rs +++ b/src/sed/delimited_parser.rs @@ -320,9 +320,10 @@ fn parse_character_class( if let Some(decoded) = parse_char_escape(line) { push_script_char(&mut result, decoded, character_mode); } else { - result.push(b'\\'); - result.push(line.current_byte()); - line.advance(); + // Outside the character escapes a \ is an ordinary member of the set, + // so the next character is left for the loop to handle. This is what + // makes `[a\]bc]` end at the first `]`, as GNU sed does. + result.extend_from_slice(br"\\"); } } else { result.push(line.current_byte()); @@ -1010,10 +1011,12 @@ mod tests { #[test] fn test_escaped_delimiter() { + // A backslash does not quote the `]`, so the class ends at it and + // holds `a` and a literal backslash, as GNU sed does. let mut line = char_provider_from("[a\\]bc]"); let lines = test_lines(); let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); - assert_eq!(result, br"[a\]bc]"); + assert_eq!(result, br"[a\\]"); } #[test] @@ -1253,7 +1256,8 @@ mod tests { fn test_regex_with_escaped_bracket_in_character_class() { let (lines, mut line) = make_providers("/[a\\]z]/"); let parsed = parse_regex(&lines, &mut line, RegexMode::Basic).unwrap(); - assert_eq!(parsed, br"[a\]z]"); + // The class ends at the `]`, leaving `z]` as literal characters. + assert_eq!(parsed, br"[a\\]z]"); assert_eq!(line.current(), '/'); } diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index b1136f68..fa8c2a2a 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -579,6 +579,46 @@ fn subst_multiline_flag_matches_embedded_line_end() { .stdout_is("foX\nbaX\n"); } +//////////////////////////////////////////////////////////// +// Bracket expressions +// A backslash is only special inside a bracket expression when it starts a +// character escape. Elsewhere it is an ordinary member of the set and does +// not quote the character that follows it. + +#[test] +fn test_bracket_gnu_escapes_are_literal() { + // [\w] holds a backslash and a w, it is not the GNU word class. + new_ucmd!() + .args(&["-e", r"s/[\w]/X/g"]) + .pipe_in("a\\w\n") + .succeeds() + .stdout_is("aXX\n"); +} + +#[test] +fn test_bracket_ends_at_escaped_closing_bracket() { + // The class is [a\], so `bc]` are literal characters that follow it. + new_ucmd!() + .args(&["-e", r"s/[a\]bc]/X/g"]) + .pipe_in("abc]z\na]z\n") + .succeeds() + .stdout_is("Xz\na]z\n"); +} + +#[test] +fn test_bracket_keeps_escapes_and_ranges() { + new_ucmd!() + .args(&["-e", r"s/[\t]/X/g"]) + .pipe_in("a\tb\n") + .succeeds() + .stdout_is("aXb\n"); + new_ucmd!() + .args(&["-e", r"s/[a-zA-Z]/X/g"]) + .pipe_in("a1Z\n") + .succeeds() + .stdout_is("X1X\n"); +} + // Check appropriate selection and behavior of fast_Regex matcher // Literal matcher check_output!(subst_literal_start, ["-e", r"s/^l1/L1/", LINES1]); From 4d29500b0853d772204519779f2cdbbb25703c86 Mon Sep 17 00:00:00 2001 From: LoukasPap Date: Wed, 7 Oct 2026 18:37:09 +0300 Subject: [PATCH 4/4] Address review comments on bracket expressions A '[' inside a bracket expression that does not open a POSIX class, collating symbol or equivalence is an ordinary member of the set. It was emitted together with the character that followed it, so the ']' in `[\[]` was swallowed and the expression was reported as unterminated. sed has no \b character escape, so remove it from parse_char_escape. A backslash in `[\b]` is an ordinary member of the set, '\b' in a regular expression is a word boundary, and in text and transliteration strings it stands for a bare 'b'. Disable the remaining GNU character escapes inside bracket expressions under --posix. They stay available outside them, as GNU sed does, so `[\t]` matches a backslash or a t while `\t` still matches a tab. --- src/sed/compiler.rs | 2 +- src/sed/delimited_parser.rs | 62 ++++++++++++++++++------------------- tests/by-util/test_sed.rs | 61 ++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 33 deletions(-) diff --git a/src/sed/compiler.rs b/src/sed/compiler.rs index 304d3a69..c08b7113 100644 --- a/src/sed/compiler.rs +++ b/src/sed/compiler.rs @@ -3584,7 +3584,7 @@ mod tests { compile_text_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap(); match &cmd.data { CommandData::Text(text) => { - assert_eq!(text.as_ref(), b">helll\x08o\nto\nall\x07\n"); + assert_eq!(text.as_ref(), b">helllbo\nto\nall\x07\n"); } _ => panic!("Expected CommandData::Text"), } diff --git a/src/sed/delimited_parser.rs b/src/sed/delimited_parser.rs index 0072ce47..cfb7b9b3 100644 --- a/src/sed/delimited_parser.rs +++ b/src/sed/delimited_parser.rs @@ -132,10 +132,6 @@ pub fn parse_char_escape(line: &mut ScriptCharProvider) -> Option { line.advance(); Some('\x07') } - 'b' => { - line.advance(); - Some('\x08') - } 'f' => { line.advance(); Some('\x0c') @@ -221,10 +217,13 @@ pub fn parse_char_escape(line: &mut ScriptCharProvider) -> Option { /// This functionality is needed to avoid terminating delimited /// sequences when a delimiter appears within a character class. /// While at it, handle escaped characters for the sake of consistency. +/// Under --posix the GNU character escapes are disabled here, although +/// they remain available outside bracket expressions. fn parse_character_class( lines: &ScriptLineProvider, line: &mut ScriptCharProvider, character_mode: CharacterMode, + posix: bool, ) -> UResult> { let mut result = Vec::new(); @@ -304,10 +303,8 @@ fn parse_character_class( continue; } - // Not a POSIX construct — treat as literal - result.push(b'['); - result.push(line.current_byte()); - line.advance(); + // Not a POSIX construct — an ordinary member of the set + result.extend_from_slice(br"\["); continue; } @@ -317,7 +314,8 @@ fn parse_character_class( if line.eol() { break; } - if let Some(decoded) = parse_char_escape(line) { + let decoded = if posix { None } else { parse_char_escape(line) }; + if let Some(decoded) = decoded { push_script_char(&mut result, decoded, character_mode); } else { // Outside the character escapes a \ is an ordinary member of the set, @@ -363,8 +361,8 @@ pub fn parse_regex( parse_regex_for_mode(lines, line, regex_mode, CharacterMode::Utf8, false) } -/// Return true if a backslash followed by `c` is a GNU ERE -/// that must be emitted as the literal character `c` under --posix. +/// Return true if a backslash followed by `c` is a GNU regular expression +/// extension that must be emitted as the literal character `c` under --posix. /// In Extended mode `? + |` are excluded, because `\?` is already a literal there. fn is_gnu_regex_escape(c: char, regex_mode: RegexMode) -> bool { match c { @@ -389,7 +387,7 @@ pub fn parse_regex_for_mode( while !line.eol() { match line.current() { '[' if delimiter != '[' => { - let cc = parse_character_class(lines, line, character_mode)?; + let cc = parse_character_class(lines, line, character_mode, posix)?; result.extend_from_slice(&cc); continue; } @@ -957,7 +955,7 @@ mod tests { fn test_basic_character_class() { let mut line = char_provider_from("[qr]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[qr]"); } @@ -965,7 +963,7 @@ mod tests { fn test_negated_class() { let mut line = char_provider_from("[^abc]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[^abc]"); } @@ -973,7 +971,7 @@ mod tests { fn test_leading_close_bracket() { let mut line = char_provider_from("[]abc]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[]abc]"); } @@ -981,7 +979,7 @@ mod tests { fn test_leading_negated_close_bracket() { let mut line = char_provider_from("[^]abc]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[^]abc]"); } @@ -989,7 +987,7 @@ mod tests { fn test_escaped_character_begin() { let mut line = char_provider_from("[\\nabc]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[\nabc]"); } @@ -997,7 +995,7 @@ mod tests { fn test_escaped_character_middle() { let mut line = char_provider_from("[a\\nbc]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[a\nbc]"); } @@ -1005,7 +1003,7 @@ mod tests { fn test_escaped_character_end() { let mut line = char_provider_from("[abc\\n]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[abc\n]"); } @@ -1015,7 +1013,7 @@ mod tests { // holds `a` and a literal backslash, as GNU sed does. let mut line = char_provider_from("[a\\]bc]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, br"[a\\]"); } @@ -1023,7 +1021,7 @@ mod tests { fn test_posix_class() { let mut line = char_provider_from("[[:digit:]]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[[:digit:]]"); } @@ -1031,7 +1029,7 @@ mod tests { fn test_colon_literal_character_class() { let mut line = char_provider_from("[:]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[:]"); } @@ -1039,7 +1037,7 @@ mod tests { fn test_equivalence_class() { let mut line = char_provider_from("[[=a=]]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[[=a=]]"); } @@ -1047,7 +1045,7 @@ mod tests { fn test_collating_symbol() { let mut line = char_provider_from("[[.ch.]]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); assert_eq!(result, b"[[.ch.]]"); } @@ -1055,7 +1053,7 @@ mod tests { fn test_unterminated_class_error() { let mut line = char_provider_from("[abc"); // missing closing ] let lines = test_lines(); - let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8); + let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false); assert!(err.is_err()); } @@ -1063,7 +1061,7 @@ mod tests { fn test_open_bracket_at_eol_errors() { let mut line = char_provider_from("["); let lines = test_lines(); - let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap_err(); + let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap_err(); assert!(err.to_string().contains("Unterminated bracket expression")); } @@ -1071,7 +1069,7 @@ mod tests { fn test_unterminated_posix_class_error() { let mut line = char_provider_from("[[:digit:]"); let lines = test_lines(); - let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8); + let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false); assert!(err.is_err()); } @@ -1079,7 +1077,7 @@ mod tests { fn test_unterminated_escape_error() { let mut line = char_provider_from("[abc\\"); // missing closing ] let lines = test_lines(); - let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8); + let err = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false); assert!(err.is_err()); } @@ -1087,16 +1085,16 @@ mod tests { fn test_malformed_posix_like_pattern_treated_as_literal() { let mut line = char_provider_from("[[x]yz]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); - assert_eq!(result, b"[[x]"); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); + assert_eq!(result, br"[\[x]"); } #[test] fn test_literal_open_bracket_in_character_class() { let mut line = char_provider_from("[a[b]"); let lines = test_lines(); - let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8).unwrap(); - assert_eq!(result, b"[a[b]"); + let result = parse_character_class(&lines, &mut line, CharacterMode::Utf8, false).unwrap(); + assert_eq!(result, br"[a\[b]"); } // parse_regex diff --git a/tests/by-util/test_sed.rs b/tests/by-util/test_sed.rs index fa8c2a2a..a0313ac9 100644 --- a/tests/by-util/test_sed.rs +++ b/tests/by-util/test_sed.rs @@ -619,6 +619,67 @@ fn test_bracket_keeps_escapes_and_ranges() { .stdout_is("X1X\n"); } +#[test] +fn test_posix_disables_gnu_character_escapes_in_brackets() { + // Outside a bracket expression --posix keeps them, see + // test_posix_keeps_gnu_character_escapes. + new_ucmd!() + .args(&["--posix", "-e", r"s/[\t]/X/g"]) + .pipe_in("a\tb\n") + .succeeds() + .stdout_is("a\tb\n"); + new_ucmd!() + .args(&["--posix", "-e", r"s/[\t]/X/g"]) + .pipe_in("a\\tb\n") + .succeeds() + .stdout_is("aXXb\n"); + // \n is POSIX outside bracket expressions, but GNU sed disables it + // here as well, so the newline added by N is not matched. + new_ucmd!() + .args(&["--posix", "-e", r"N;s/[\n]/X/"]) + .pipe_in("a\nb\n") + .succeeds() + .stdout_is("a\nb\n"); +} + +#[test] +fn test_bracket_escaped_delimiter_is_two_members() { + // The backslash does not quote the delimiter, so the class holds both + // a backslash and a slash. + new_ucmd!() + .args(&["-e", r"s/[\/]/X/g"]) + .pipe_in("a\\b/c\n") + .succeeds() + .stdout_is("aXbXc\n"); + new_ucmd!() + .args(&["-e", r"s,[\,],X,g"]) + .pipe_in("a\\b/c\n") + .succeeds() + .stdout_is("aXb/c\n"); +} + +#[test] +fn test_bracket_literal_open_bracket() { + // [\[] and [[] both hold a literal [, they do not open a nested class. + for script in [r"s/[\[]/X/g", r"s/[[]/X/g"] { + new_ucmd!() + .args(&["-e", script]) + .pipe_in("a[b\n") + .succeeds() + .stdout_is("aXb\n"); + } +} + +#[test] +fn test_bracket_backslash_b_is_not_backspace() { + // sed has no \b character escape, so [\b] holds a backslash and a b. + new_ucmd!() + .args(&["-e", r"s/[\b]/X/g"]) + .pipe_in("a\\b\n") + .succeeds() + .stdout_is("aXX\n"); +} + // Check appropriate selection and behavior of fast_Regex matcher // Literal matcher check_output!(subst_literal_start, ["-e", r"s/^l1/L1/", LINES1]);