Repository: reubeno/brush
Pull request #309 (2025-01-09 04:04:10)

feat(builtins): implement times builtin
Implement the `times` builtin, but only on Unix-based platforms (i.e., Linux, macOS). We're relying on `nix` and its wrapper for `getrusage` as implementation.

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   19.35 μs` | `   19.67 μs` | `    0.32 μs` | `⚪  Unchanged` |
| `eval_arithmetic                     ` | `    0.46 μs` | `    0.43 μs` | `   -0.02 μs` | `⚪  Unchanged` |
| `expand_one_string                   ` | `    2.28 μs` | `    2.38 μs` | `    0.09 μs` | `⚪  Unchanged` |
| `for_loop                            ` | `   64.22 μs` | `   65.19 μs` | `    0.97 μs` | `🟠 +1.51%    ` |
| `function_call                       ` | `    5.06 μs` | `    5.24 μs` | `    0.17 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   61.76 μs` | `   61.87 μs` | `    0.11 μs` | `⚪  Unchanged` |
| `instantiate_shell_with_init_scripts ` | `30596.66 μs` | `29412.31 μs` | `-1184.35 μs` | `🟢 -3.87%    ` |
| `parse_bash_completion               ` | ` 2889.99 μs` | ` 2834.01 μs` | `  -55.98 μs` | `🟢 -1.94%    ` |
| `parse_sample_script                 ` | `    4.44 μs` | `    4.34 μs` | `   -0.10 μs` | `🟢 -2.36%    ` |
| `run_echo_builtin_command            ` | `   22.96 μs` | `   21.99 μs` | `   -0.98 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 1715.73 μs` | ` 1899.12 μs` | `  183.39 μs` | `🟠 +10.69%   ` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/builtins/factory.rs | 🟢 82.5% | 🟢 82.28% | 🔴 -0.22% |
| brush-core/src/builtins/times.rs | 🔴 0% | 🟢 100% | 🟢 100% |
| brush-core/src/jobs.rs | 🔴 37.07% | 🔴 43.1% | 🟢 6.03% |
| brush-core/src/sys/unix/signal.rs | 🟠 64.41% | 🟠 61.02% | 🔴 -3.39% |
| **Overall Coverage** | **🟢 76.8%** | **🟢 76.92%** | **🟢 0.12%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `76.92%`
<!-- Sticky Pull Request Comment -->
github-actions[bot]: ## Test Results
    2 files      9 suites   1m 32s ⏱️
  573 tests   573 ✅ 0 💤 0 ❌
1 132 runs  1 132 ✅ 0 💤 0 ❌

Results for commit eedf6d0f.

[test-results]:data:application/gzip;base64,H4sIAM5Mf2cC/12MSw7CIBQAr9KwdsHHFvUyhvLeS4htMXxWxrsLpNXW5cwk82LkJozs1slTx2J2qcG1AORgkvNLxRpLSbX1Wm10j9naf/Vwz6L4V5Bx00FgCD6sJuSlPoVQcqXteVC/Z+PdsvH+aP08u1SAIQINwEkijqBBKX7pz0RaK2G1gFELQ1bSwN4f12jlFAYBAAA=


Patch:
diff --git a/brush-core/Cargo.toml b/brush-core/Cargo.toml
index 330a431..87ed848 100644
--- a/brush-core/Cargo.toml
+++ b/brush-core/Cargo.toml
@@ -57,6 +57,7 @@ command-fds = "0.3.0"
 nix = { version = "0.29.0", features = [
     "fs",
     "process",
+    "resource",
     "signal",
     "term",
     "user",
diff --git a/brush-core/src/builtins.rs b/brush-core/src/builtins.rs
index 0591485..6ac8fe1 100644
--- a/brush-core/src/builtins.rs
+++ b/brush-core/src/builtins.rs
@@ -50,6 +50,8 @@ mod set;
 mod shift;
 mod shopt;
 mod test;
+#[cfg(unix)]
+mod times;
 mod trap;
 mod true_;
 mod type_;
diff --git a/brush-core/src/builtins/factory.rs b/brush-core/src/builtins/factory.rs
index 4261552..a6533a1 100644
--- a/brush-core/src/builtins/factory.rs
+++ b/brush-core/src/builtins/factory.rs
@@ -219,10 +219,8 @@ pub(crate) fn get_default_builtins(
         "readonly".into(),
         special_decl_builtin::<declare::DeclareCommand>(),
     );
-    m.insert(
-        "times".into(),
-        special_builtin::<unimp::UnimplementedCommand>(),
-    );
+    #[cfg(unix)]
+    m.insert("times".into(), special_builtin::<times::TimesCommand>());
 
     //
     // Non-special builtins
diff --git a/brush-core/src/builtins/times.rs b/brush-core/src/builtins/times.rs
new file mode 100644
index 0000000..e20ef07
--- /dev/null
+++ b/brush-core/src/builtins/times.rs
@@ -0,0 +1,76 @@
+use clap::Parser;
+use std::io::Write;
+
+use crate::{builtins, commands, error};
+
+/// Report on usage time.
+#[derive(Parser)]
+pub(crate) struct TimesCommand {}
+
+impl builtins::Command for TimesCommand {
+    async fn execute(
+        &self,
+        context: commands::ExecutionContext<'_>,
+    ) -> Result<builtins::ExitCode, error::Error> {
+        let self_usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF)?;
+
+        writeln!(
+            context.stdout(),
+            "{} {}",
+            format_time(self_usage.user_time()),
+            format_time(self_usage.system_time()),
+        )?;
+
+        let children_usage =
+            nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_CHILDREN)?;
+
+        writeln!(
+            context.stdout(),
+            "{} {}",
+            format_time(children_usage.user_time()),
+            format_time(children_usage.system_time()),
+        )?;
+
+        Ok(builtins::ExitCode::Success)
+    }
+}
+
+fn format_time(time: nix::sys::time::TimeVal) -> String {
+    #[allow(clippy::cast_sign_loss)]
+    #[allow(clippy::cast_possible_truncation)]
+    let duration = std::time::Duration::new(time.tv_sec() as u64, time.tv_usec() as u32 * 1000);
+    let minutes = duration.as_secs() / 60;
+    let seconds = duration.as_secs() % 60;
+    let millis = duration.subsec_millis();
+    format!("{minutes}m{seconds}.{millis:03}s")
+}
+
+#[cfg(test)]
+mod tests {
+    use nix::sys::time::TimeValLike;
+
+    use super::*;
+
+    #[test]
+    fn test_format_time() {
+        fn ms_to_timeval(ms: i64) -> nix::sys::time::TimeVal {
+            nix::sys::time::TimeVal::milliseconds(ms)
+        }
+
+        fn us_to_timeval(us: i64) -> nix::sys::time::TimeVal {
+            nix::sys::time::TimeVal::microseconds(us)
+        }
+
+        assert_eq!(format_time(ms_to_timeval(0)), "0m0.000s");
+        assert_eq!(format_time(ms_to_timeval(1)), "0m0.001s");
+        assert_eq!(format_time(ms_to_timeval(123)), "0m0.123s");
+        assert_eq!(format_time(ms_to_timeval(1234)), "0m1.234s");
+        assert_eq!(format_time(ms_to_timeval(12345)), "0m12.345s");
+        assert_eq!(format_time(ms_to_timeval(123456)), "2m3.456s");
+        assert_eq!(format_time(ms_to_timeval(1234567)), "20m34.567s");
+
+        assert_eq!(format_time(us_to_timeval(1)), "0m0.000s");
+        assert_eq!(format_time(us_to_timeval(999)), "0m0.000s");
+        assert_eq!(format_time(us_to_timeval(1000)), "0m0.001s");
+    }
+}


Test patch:
diff --git a/brush-shell/tests/cases/builtins/times.yaml b/brush-shell/tests/cases/builtins/times.yaml
new file mode 100644
index 0000000..a351226
--- /dev/null
+++ b/brush-shell/tests/cases/builtins/times.yaml
@@ -0,0 +1,6 @@
+name: "Builtins: times"
+cases:
+  - name: "Basic usage"
+    ignore_stdout: true
+    stdin: |
+      times