Repository: reubeno/brush
Pull request #439 (2025-04-23 14:39:11)

feat(apply_unary_predicate_to_str): implement `-R` operand
Adds:

* `-R`
* `-ef`
* `-nt`
* `-ot`

Elsie19: I'm going to be working on `[[ ]]` a bit more which is why this is a draft.
github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   18.02 μs` | `   17.92 μs` | `   -0.10 μs` | `🟢 -0.57%    ` |
| `eval_arithmetic                     ` | `    0.17 μs` | `    0.17 μs` | `    0.00 μs` | `⚪  Unchanged` |
| `expand_one_string                   ` | `    1.99 μs` | `    1.93 μs` | `   -0.07 μs` | `⚪  Unchanged` |
| `for_loop                            ` | `   23.23 μs` | `   23.86 μs` | `    0.63 μs` | `🟠 +2.72%    ` |
| `function_call                       ` | `    2.71 μs` | `    2.77 μs` | `    0.06 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   57.46 μs` | `   57.79 μs` | `    0.33 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `25598.81 μs` | `25142.81 μs` | ` -456.01 μs` | `⚪  Unchanged` |
| `parse_bash_completion               ` | ` 1661.46 μs` | ` 1688.07 μs` | `   26.60 μs` | `🟠 +1.60%    ` |
| `parse_sample_script                 ` | `    1.74 μs` | `    1.82 μs` | `    0.08 μs` | `🟠 +4.43%    ` |
| `run_echo_builtin_command            ` | `   16.27 μs` | `   16.17 μs` | `   -0.10 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 5125.75 μs` | ` 3947.16 μs` | `-1178.58 μs` | `🟢 -22.99%   ` |
| `tokenize_sample_script              ` | `    2.82 μs` | `    2.79 μs` | `   -0.03 μs` | `⚪  Unchanged` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/extendedtests.rs | 🟠 62.29% | 🟠 61.94% | 🔴 -0.35% |
| brush-core/src/sys/unix/signal.rs | 🟠 61.02% | 🟠 64.41% | 🟢 3.39% |
| **Overall Coverage** | **🟢 78.24%** | **🟢 78.24%** | **⚪ 0%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `78.24%`

# Test Summary: bash-completion test suite
| Outcome            | Count                   | Percentage |
| ------------------ | ----------------------: | ---------: |
| ✅ Pass            | 1309            | <span style='color:green'>62.07</span> |
| ❗️ Error           | 227           | <span style='color:red'>10.76</span> |
| ❌ Fail            | 194            | <span style='color:red'>9.20</span> |
| ⏩ Skip            | 364            | 17.26 |
| ❎ Expected Fail   | 14   | 0.66 |
| ✔️ Unexpected Pass | 1 | <span style='color:red'>0.05</span> |
| 📊 Total           | 2109           | 100.00 |

<!-- Sticky Pull Request Comment -->
Elsie19: Unfortunately I introduced a `.except()` in here which I'm sure you don't want, and I'm unsure of how to move forward.
Elsie19: I think I am confusing myself more 😄... I'll just wait for some input on how to move forward.
Elsie19: Before merging, if you want, I can do some squash commits to make the merge look a lot less like *that* :)
Elsie19: I think it's ready now!

Patch:
diff --git a/brush-core/src/sys/fs.rs b/brush-core/src/sys/fs.rs
index d78fbf1..8c62d1a 100644
--- a/brush-core/src/sys/fs.rs
+++ b/brush-core/src/sys/fs.rs
@@ -18,4 +18,6 @@ pub(crate) trait PathExt {
     fn exists_and_is_setgid(&self) -> bool;
     fn exists_and_is_setuid(&self) -> bool;
     fn exists_and_is_sticky_bit(&self) -> bool;
+
+    fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error>;
 }
diff --git a/brush-core/src/sys/stubs/fs.rs b/brush-core/src/sys/stubs/fs.rs
index 2ed31d6..3131b5e 100644
--- a/brush-core/src/sys/stubs/fs.rs
+++ b/brush-core/src/sys/stubs/fs.rs
@@ -39,6 +39,10 @@ impl crate::sys::fs::PathExt for std::path::Path {
     fn exists_and_is_sticky_bit(&self) -> bool {
         false
     }
+
+    fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
+        Ok((0, 0))
+    }
 }
 
 pub(crate) trait StubMetadataExt {
diff --git a/brush-core/src/sys/unix/fs.rs b/brush-core/src/sys/unix/fs.rs
index e70259f..64ea98c 100644
--- a/brush-core/src/sys/unix/fs.rs
+++ b/brush-core/src/sys/unix/fs.rs
@@ -60,6 +60,11 @@ impl crate::sys::fs::PathExt for Path {
         let file_mode = try_get_file_mode(self);
         file_mode.is_some_and(|mode| mode & S_ISVTX != 0)
     }
+
+    fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
+        let metadata = self.metadata()?;
+        Ok((metadata.dev(), metadata.ino()))
+    }
 }
 
 fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
diff --git a/brush-core/src/variables.rs b/brush-core/src/variables.rs
index 5c81af9..8a0597b 100644
--- a/brush-core/src/variables.rs
+++ b/brush-core/src/variables.rs
@@ -666,6 +666,11 @@ impl ShellValue {
         )
     }
 
+    /// Returns whether or not the value is set.
+    pub fn is_set(&self) -> bool {
+        !matches!(self, ShellValue::Unset(_))
+    }
+
     /// Returns a new indexed array value constructed from the given slice of owned strings.
     ///
     /// # Arguments


Test patch:
diff --git a/brush-core/src/extendedtests.rs b/brush-core/src/extendedtests.rs
index 3174c33..0bcfbc7 100644
--- a/brush-core/src/extendedtests.rs
+++ b/brush-core/src/extendedtests.rs
@@ -181,9 +181,10 @@ pub(crate) fn apply_unary_predicate_to_str(
             }
         }
         ast::UnaryPredicate::ShellVariableIsSetAndAssigned => Ok(shell.env.is_set(operand)),
-        ast::UnaryPredicate::ShellVariableIsSetAndNameRef => {
-            error::unimp("unary extended test predicate: ShellVariableIsSetAndNameRef")
-        }
+        ast::UnaryPredicate::ShellVariableIsSetAndNameRef => match shell.env.get(operand) {
+            Some((_, reffed)) => Ok(reffed.value().is_set() && reffed.is_treated_as_nameref()),
+            None => Ok(false),
+        },
     }
 }
 
@@ -275,14 +276,68 @@ async fn apply_binary_predicate(
             Ok(s.contains(substring.as_str()))
         }
         ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => {
-            error::unimp("extended test binary predicate FilesReferToSameDeviceAndInodeNumbers")
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
+
+            if shell.options.print_commands_and_arguments {
+                shell
+                    .trace_command(std::format!("[[ {left} {op} {right} ]]"))
+                    .await?;
+            }
+
+            let (l_path, r_path) = (
+                shell.get_absolute_path(Path::new(&left)),
+                shell.get_absolute_path(Path::new(&right)),
+            );
+
+            if !l_path.readable() || !r_path.readable() {
+                return Ok(false);
+            }
+
+            Ok(l_path.get_device_and_inode()? == r_path.get_device_and_inode()?)
         }
         ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => {
-            error::unimp("extended test binary predicate LeftFileIsNewerOrExistsWhenRightDoesNot")
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
+
+            if shell.options.print_commands_and_arguments {
+                shell
+                    .trace_command(std::format!("[[ {left} {op} {right} ]]"))
+                    .await?;
+            }
+
+            let (l_path, r_path) = (
+                shell.get_absolute_path(Path::new(&left)),
+                shell.get_absolute_path(Path::new(&right)),
+            );
+
+            match (l_path.metadata(), r_path.metadata()) {
+                (Ok(m1), Ok(m2)) => Ok(m1.modified()? > m2.modified()?),
+                (Ok(_), Err(_)) => Ok(true),
+                _ => Ok(false),
+            }
+        }
+        ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => {
+            let left = expansion::basic_expand_word(shell, params, left).await?;
+            let right = expansion::basic_expand_word(shell, params, right).await?;
+
+            if shell.options.print_commands_and_arguments {
+                shell
+                    .trace_command(std::format!("[[ {left} {op} {right} ]]"))
+                    .await?;
+            }
+
+            let (l_path, r_path) = (
+                shell.get_absolute_path(Path::new(&left)),
+                shell.get_absolute_path(Path::new(&right)),
+            );
+
+            match (l_path.metadata(), r_path.metadata()) {
+                (Ok(m1), Ok(m2)) => Ok(m1.modified()? < m2.modified()?),
+                (Err(_), Ok(_)) => Ok(true),
+                _ => Ok(false),
+            }
         }
-        ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => error::unimp(
-            "extended test binary predicate LeftFileIsOlderOrDoesNotExistWhenRightDoes",
-        ),
         ast::BinaryPredicate::LeftSortsBeforeRight => {
             let left = expansion::basic_expand_word(shell, params, left).await?;
             let right = expansion::basic_expand_word(shell, params, right).await?;
diff --git a/brush-shell/tests/cases/extended_tests.yaml b/brush-shell/tests/cases/extended_tests.yaml
index 0016570..1d9a842 100644
--- a/brush-shell/tests/cases/extended_tests.yaml
+++ b/brush-shell/tests/cases/extended_tests.yaml
@@ -110,6 +110,37 @@ cases:
       [[ -w test-file ]] && echo "-w correctly identified writable file"
       [[ -w link ]] && echo "-w correctly identified link to writable file"
 
+  - name: "Variable set and nameref tests"
+    stdin: |
+      foo="bar"
+      declare -n bang=foo
+      [[ -R bang ]] && echo "-R correctly identified nameref variable that is set"
+
+      declare -n somevar
+      [[ -R somevar ]] || echo "-R correctly identified nameref variable that isn't set"
+
+  - name: "Files refer to same device and inode tests"
+    stdin: |
+      [[ /bin/sh -ef /bin/sh ]] && echo "-ef correctly identified device and inode numbers"
+
+      [[ ! /etc/os-release -ef /bin/sh ]] && echo "-ef correctly identified device and inode numbers that do not match"
+
+  - name: "File is newer"
+    stdin: |
+      touch bar
+      touch foo
+      [[ foo -nt bar ]] && echo "-nt correctly identified newer file"
+
+      [[ foo -nt file_no_exists ]] && echo "-nt correctly identified when file2 does not exist"
+
+  - name: "File is older"
+    stdin: |
+      touch foo
+      touch bar
+      [[ foo -ot bar ]] && echo "-ot correctly identified older file"
+
+      [[ file_no_exists -ot foo ]] && echo "-ot correctly identified when file1 does not exist"
+
   - name: "Unary string extended tests"
     stdin: |
       [[ -z "" ]] && echo "-z: Pass"