Repository: reubeno/brush (rust)

Issue:
[ "foo" = "fo*" ] should not have a pattern on the right side of the =
```console
$ brush --version
brush 0.2.14 (git:dd1d39c)

$ brush -c '[ "foo" = "fo?" ] && echo match || echo unmatch'
match

$ bash -c '[ "foo" = "fo?" ] && echo match || echo unmatch'
unmatch
```

Pull request description:
fix: correct semantics of = in non-extended test commands
In a non-extended test command (e.g., `[` or `test`), the `=`, `==`, and `!=` operators are direct, case-sensitive string comparisons -- with no pattern matching. These changes correct that behavior.

(The incorrect behavior was inadvertently inherited from `[[`-style extended test commands when originally implemented.)

Resolves #340. Thanks to @ko1nksm for identifying the problem and filing a bug report on it!

Patch:
diff --git a/brush-parser/src/ast.rs b/brush-parser/src/ast.rs
index 5979413..2674707 100644
--- a/brush-parser/src/ast.rs
+++ b/brush-parser/src/ast.rs
@@ -1150,6 +1150,10 @@ pub enum BinaryPredicate {
     StringDoesNotExactlyMatchPattern,
     /// Computes if a string matches a regular expression.
     StringMatchesRegex,
+    /// Computes if a string exactly matches another string.
+    StringExactlyMatchesString,
+    /// Computes if a string does not exactly match another string.
+    StringDoesNotExactlyMatchString,
     /// Computes if a string contains a substring.
     StringContainsSubstring,
     /// Computes if the left value sorts before the right.
@@ -1180,6 +1184,8 @@ impl Display for BinaryPredicate {
             BinaryPredicate::StringDoesNotExactlyMatchPattern => write!(f, "!="),
             BinaryPredicate::StringMatchesRegex => write!(f, "=~"),
             BinaryPredicate::StringContainsSubstring => write!(f, "=~"),
+            BinaryPredicate::StringExactlyMatchesString => write!(f, "=="),
+            BinaryPredicate::StringDoesNotExactlyMatchString => write!(f, "!="),
             BinaryPredicate::LeftSortsBeforeRight => write!(f, "<"),
             BinaryPredicate::LeftSortsAfterRight => write!(f, ">"),
             BinaryPredicate::ArithmeticEqualTo => write!(f, "-eq"),


Test patch:
diff --git a/brush-core/src/extendedtests.rs b/brush-core/src/extendedtests.rs
index 6cc4140..1c18071 100644
--- a/brush-core/src/extendedtests.rs
+++ b/brush-core/src/extendedtests.rs
@@ -226,6 +226,26 @@ async fn apply_binary_predicate(
 
             Ok(matches)
         }
+        ast::BinaryPredicate::StringExactlyMatchesString => {
+            let left = expansion::basic_expand_word(shell, left).await?;
+            let right = expansion::basic_expand_word(shell, right).await?;
+
+            if shell.options.print_commands_and_arguments {
+                shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
+            }
+
+            Ok(left == right)
+        }
+        ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
+            let left = expansion::basic_expand_word(shell, left).await?;
+            let right = expansion::basic_expand_word(shell, right).await?;
+
+            if shell.options.print_commands_and_arguments {
+                shell.trace_command(std::format!("[[ {left} {op} {right} ]]"))?;
+            }
+
+            Ok(left != right)
+        }
         ast::BinaryPredicate::StringContainsSubstring => {
             let s = expansion::basic_expand_word(shell, left).await?;
             let substring = expansion::basic_expand_word(shell, right).await?;
@@ -436,6 +456,8 @@ pub(crate) fn apply_binary_predicate_to_strs(
             let eq = pattern.exactly_matches(left)?;
             Ok(!eq)
         }
+        ast::BinaryPredicate::StringExactlyMatchesString => Ok(left == right),
+        ast::BinaryPredicate::StringDoesNotExactlyMatchString => Ok(left != right),
         _ => error::unimp("unsupported test binary predicate"),
     }
 }
diff --git a/brush-parser/src/test_command.rs b/brush-parser/src/test_command.rs
index 82df7dd..03a0b25 100644
--- a/brush-parser/src/test_command.rs
+++ b/brush-parser/src/test_command.rs
@@ -89,7 +89,7 @@ peg::parser! {
             ["-S"] { ast::UnaryPredicate::FileExistsAndIsSocket }
 
         rule binary_op() -> ast::BinaryPredicate =
-            ["=="] { ast::BinaryPredicate::StringExactlyMatchesPattern } /
+            ["=="]  { ast::BinaryPredicate::StringExactlyMatchesString } /
             ["-ef"] { ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers } /
             ["-eq"] { ast::BinaryPredicate::ArithmeticEqualTo } /
             ["-ge"] { ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo } /
@@ -99,11 +99,10 @@ peg::parser! {
             ["-ne"] { ast::BinaryPredicate::ArithmeticNotEqualTo } /
             ["-nt"] { ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot } /
             ["-ot"] { ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes } /
-            ["=="] { ast::BinaryPredicate::StringExactlyMatchesPattern } /
-            ["="] { ast::BinaryPredicate::StringExactlyMatchesPattern } /
-            ["!="] { ast::BinaryPredicate::StringDoesNotExactlyMatchPattern } /
-            ["<"] { ast::BinaryPredicate::LeftSortsBeforeRight } /
-            [">"] { ast::BinaryPredicate::LeftSortsAfterRight }
+            ["="]   { ast::BinaryPredicate::StringExactlyMatchesString } /
+            ["!="]  { ast::BinaryPredicate::StringDoesNotExactlyMatchString } /
+            ["<"]   { ast::BinaryPredicate::LeftSortsBeforeRight } /
+            [">"]   { ast::BinaryPredicate::LeftSortsAfterRight }
 
         rule end() = ![_]
     }
diff --git a/brush-shell/tests/cases/builtins/test.yaml b/brush-shell/tests/cases/builtins/test.yaml
new file mode 100644
index 0000000..fa0f9b8
--- /dev/null
+++ b/brush-shell/tests/cases/builtins/test.yaml
@@ -0,0 +1,29 @@
+name: "Builtins: test"
+cases:
+  - name: "test: = operator"
+    stdin: |
+      shopt -u nocasematch
+      test "ab" = "ab" && echo "ab = ab"
+      test "ab" = "AB" && echo "ab = AB"
+      test "ab" = "cd" && echo "ab = cd"
+      test "ab" = "a?" && echo "ab = a?"
+
+      shopt -s nocasematch
+      test "ab" = "ab" && echo "ab = ab"
+      test "ab" = "AB" && echo "ab = AB"
+      test "ab" = "cd" && echo "ab = cd"
+      test "ab" = "a?" && echo "ab = a?"
+
+  - name: "test: == operator"
+    stdin: |
+      shopt -u nocasematch
+      test "ab" == "ab" && echo "ab == ab"
+      test "ab" == "AB" && echo "ab == AB"
+      test "ab" == "cd" && echo "ab == cd"
+      test "ab" == "a?" && echo "ab == a?"
+
+      shopt -s nocasematch
+      test "ab" == "ab" && echo "ab == ab"
+      test "ab" == "AB" && echo "ab == AB"
+      test "ab" == "cd" && echo "ab == cd"
+      test "ab" == "a?" && echo "ab == a?"