From b0390fe36e5a7404f7ddb83fb1131974b61e835d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sat, 24 May 2025 01:28:53 +0300 Subject: [PATCH] expr: Handle `$` at the beginning of the regex pattern --- src/uu/expr/src/syntax_tree.rs | 39 +++++++++++++++++++++------------- tests/by-util/test_expr.rs | 16 ++++++++++++++ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/uu/expr/src/syntax_tree.rs b/src/uu/expr/src/syntax_tree.rs index 11103ee4b..9b80d40c2 100644 --- a/src/uu/expr/src/syntax_tree.rs +++ b/src/uu/expr/src/syntax_tree.rs @@ -161,6 +161,7 @@ impl StringOp { match first { Some('^') => {} // Start of string anchor is already added Some('*') => re_string.push_str(r"\*"), + Some('$') if !is_end_of_expression(&pattern_chars) => re_string.push_str(r"\$"), Some('\\') if right.len() == 1 => return Err(ExprError::TrailingBackslash), Some(char) => re_string.push(char), None => return Ok(0.into()), @@ -185,23 +186,12 @@ impl StringOp { _ => re_string.push_str(r"\^"), }, '$' => { - if let Some('\\') = pattern_chars.peek() { - // The next character was checked to be a backslash - let backslash = pattern_chars.next().unwrap_or_default(); - match pattern_chars.peek() { - // End of a capturing group - Some(')') => re_string.push('$'), - // End of an alternative pattern - Some('|') => re_string.push('$'), - _ => re_string.push_str(r"\$"), - } - re_string.push(backslash); - } else if (prev_is_escaped || prev != '\\') - && pattern_chars.peek().is_some() - { + if is_end_of_expression(&pattern_chars) { + re_string.push(curr); + } else if !curr_is_escaped { re_string.push_str(r"\$"); } else { - re_string.push('$'); + re_string.push(curr); } } '\\' if !curr_is_escaped && pattern_chars.peek().is_none() => { @@ -247,6 +237,25 @@ impl StringOp { } } +/// Check if regex pattern character iterator is at the end of a regex expression or subexpression +fn is_end_of_expression(pattern_chars: &I) -> bool +where + I: Iterator + Clone, +{ + let mut pattern_chars_clone = pattern_chars.clone(); + match pattern_chars_clone.next() { + Some('\\') => { + match pattern_chars_clone.next() { + Some(')') // End of a capturing group + | Some('|') => true, // End of an alternative pattern + _ => false, + } + } + None => true, // No characters left + _ => false, + } +} + /// Check for errors in a supplied regular expression /// /// GNU coreutils shows messages for invalid regular expressions diff --git a/tests/by-util/test_expr.rs b/tests/by-util/test_expr.rs index e301c2470..2eee64555 100644 --- a/tests/by-util/test_expr.rs +++ b/tests/by-util/test_expr.rs @@ -318,6 +318,14 @@ fn test_regex() { .args(&["a$c", ":", "a$\\c"]) .succeeds() .stdout_only("3\n"); + new_ucmd!() + .args(&["$a", ":", "$a"]) + .succeeds() + .stdout_only("2\n"); + new_ucmd!() + .args(&["a", ":", "a$\\|b"]) + .succeeds() + .stdout_only("1\n"); new_ucmd!() .args(&["^^^^^^^^^", ":", "^^^"]) .succeeds() @@ -363,6 +371,14 @@ fn test_regex() { .args(&["abc", ":", "ab[^c]"]) .fails() .stdout_only("0\n"); + new_ucmd!() + .args(&["$", ":", "$"]) + .fails() + .stdout_only("0\n"); + new_ucmd!() + .args(&["a$", ":", "a$\\|b"]) + .fails() + .stdout_only("0\n"); } #[test]