From ac9a41813f02dfa76b1215a8e54f92d18d4aecd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 05:59:48 +0000 Subject: [PATCH] feat(builtins): implement yes and realpath builtins yes: repeatedly output a string (with 10K line safety limit) realpath: resolve absolute pathname with . and .. normalization https://claude.ai/code/session_012rzB3FRw7yoQWCG1mxyW7J --- crates/bashkit/src/builtins/mod.rs | 4 +- crates/bashkit/src/builtins/path.rs | 32 ++++++++++++++++ crates/bashkit/src/builtins/yes.rs | 37 +++++++++++++++++++ crates/bashkit/src/interpreter/mod.rs | 2 + .../tests/spec_cases/bash/path.test.sh | 32 ++++++++++++++++ .../tests/spec_cases/bash/textrev.test.sh | 25 +++++++++++++ specs/009-implementation-status.md | 10 ++--- 7 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 crates/bashkit/src/builtins/yes.rs diff --git a/crates/bashkit/src/builtins/mod.rs b/crates/bashkit/src/builtins/mod.rs index 2d57968c..3a78c925 100644 --- a/crates/bashkit/src/builtins/mod.rs +++ b/crates/bashkit/src/builtins/mod.rs @@ -63,6 +63,7 @@ mod timeout; mod vars; mod wait; mod wc; +mod yes; #[cfg(feature = "git")] mod git; @@ -94,7 +95,7 @@ pub use ls::{Find, Ls, Rmdir}; pub use navigation::{Cd, Pwd}; pub use nl::Nl; pub use paste::Paste; -pub use path::{Basename, Dirname}; +pub use path::{Basename, Dirname, Realpath}; pub use pipeline::{Tee, Watch, Xargs}; pub use printf::Printf; pub use read::Read; @@ -111,6 +112,7 @@ pub use timeout::Timeout; pub use vars::{Eval, Local, Readonly, Set, Shift, Shopt, Times, Unset}; pub use wait::Wait; pub use wc::Wc; +pub use yes::Yes; #[cfg(feature = "git")] pub use git::Git; diff --git a/crates/bashkit/src/builtins/path.rs b/crates/bashkit/src/builtins/path.rs index 2412b996..5ba7165b 100644 --- a/crates/bashkit/src/builtins/path.rs +++ b/crates/bashkit/src/builtins/path.rs @@ -119,6 +119,38 @@ impl Builtin for Dirname { } } +/// The realpath builtin - resolve absolute pathname. +/// +/// Usage: realpath [PATH...] +/// +/// Resolves `.` and `..` components and prints absolute canonical paths. +/// In bashkit's virtual filesystem, symlink resolution is not performed. +pub struct Realpath; + +#[async_trait] +impl Builtin for Realpath { + async fn execute(&self, ctx: Context<'_>) -> Result { + if ctx.args.is_empty() { + return Ok(ExecResult::err( + "realpath: missing operand\n".to_string(), + 1, + )); + } + + let mut output = String::new(); + for arg in ctx.args { + if arg.starts_with('-') { + continue; // skip flags like -e, -m, -s + } + let resolved = super::resolve_path(ctx.cwd, arg); + output.push_str(&resolved.to_string_lossy()); + output.push('\n'); + } + + Ok(ExecResult::ok(output)) + } +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { diff --git a/crates/bashkit/src/builtins/yes.rs b/crates/bashkit/src/builtins/yes.rs new file mode 100644 index 00000000..a66ff68d --- /dev/null +++ b/crates/bashkit/src/builtins/yes.rs @@ -0,0 +1,37 @@ +//! yes builtin - repeatedly output a line + +use async_trait::async_trait; + +use super::{Builtin, Context}; +use crate::error::Result; +use crate::interpreter::ExecResult; + +/// The yes builtin - output a string repeatedly. +/// +/// Usage: yes [STRING] +/// +/// Repeatedly outputs STRING (default: "y") followed by newline. +/// In bashkit, output is limited to avoid infinite loops. +pub struct Yes; + +/// Maximum number of lines to output (safety limit) +const MAX_LINES: usize = 10_000; + +#[async_trait] +impl Builtin for Yes { + async fn execute(&self, ctx: Context<'_>) -> Result { + let text = if ctx.args.is_empty() { + "y".to_string() + } else { + ctx.args.join(" ") + }; + + let mut output = String::new(); + for _ in 0..MAX_LINES { + output.push_str(&text); + output.push('\n'); + } + + Ok(ExecResult::ok(output)) + } +} diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 1b506d7b..39914310 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -237,6 +237,7 @@ impl Interpreter { builtins.insert("tail".to_string(), Box::new(builtins::Tail)); builtins.insert("basename".to_string(), Box::new(builtins::Basename)); builtins.insert("dirname".to_string(), Box::new(builtins::Dirname)); + builtins.insert("realpath".to_string(), Box::new(builtins::Realpath)); builtins.insert("mkdir".to_string(), Box::new(builtins::Mkdir)); builtins.insert("mktemp".to_string(), Box::new(builtins::Mktemp)); builtins.insert("rm".to_string(), Box::new(builtins::Rm)); @@ -260,6 +261,7 @@ impl Interpreter { builtins.insert("seq".to_string(), Box::new(builtins::Seq)); builtins.insert("tac".to_string(), Box::new(builtins::Tac)); builtins.insert("rev".to_string(), Box::new(builtins::Rev)); + builtins.insert("yes".to_string(), Box::new(builtins::Yes)); builtins.insert("sort".to_string(), Box::new(builtins::Sort)); builtins.insert("uniq".to_string(), Box::new(builtins::Uniq)); builtins.insert("cut".to_string(), Box::new(builtins::Cut)); diff --git a/crates/bashkit/tests/spec_cases/bash/path.test.sh b/crates/bashkit/tests/spec_cases/bash/path.test.sh index d9dd202b..b0943399 100644 --- a/crates/bashkit/tests/spec_cases/bash/path.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/path.test.sh @@ -97,3 +97,35 @@ dirname file ### expect . ### end + +### realpath_absolute +# realpath resolves .. components +### bash_diff +realpath /tmp/../tmp/test +### expect +/tmp/test +### end + +### realpath_dot +# realpath resolves . components +### bash_diff +realpath /home/user/./file.txt +### expect +/home/user/file.txt +### end + +### realpath_dotdot +# realpath resolves parent directory references +### bash_diff +realpath /home/user/docs/../file.txt +### expect +/home/user/file.txt +### end + +### realpath_no_args +# realpath with no args should error +realpath 2>/dev/null +echo $? +### expect +1 +### end diff --git a/crates/bashkit/tests/spec_cases/bash/textrev.test.sh b/crates/bashkit/tests/spec_cases/bash/textrev.test.sh index ca2131c8..022f7378 100644 --- a/crates/bashkit/tests/spec_cases/bash/textrev.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/textrev.test.sh @@ -90,3 +90,28 @@ echo "a b c" | rev ### expect c b a ### end + +### yes_default +# yes outputs "y" by default (piped through head) +yes | head -3 +### expect +y +y +y +### end + +### yes_custom_string +# yes with custom string +yes hello | head -2 +### expect +hello +hello +### end + +### yes_multiple_args +# yes joins multiple args with space +yes a b c | head -2 +### expect +a b c +a b c +### end diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index d18e055f..162171de 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See ## Spec Test Coverage -**Total spec test cases:** 1369 (1364 pass, 5 skip) +**Total spec test cases:** 1376 (1371 pass, 5 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 951 | Yes | 946 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 958 | Yes | 953 | 5 | `bash_spec_tests` in CI | | AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g | | Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect | | Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E | | JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | | Python | 57 | Yes | 57 | 0 | embedded Python (Monty) | -| **Total** | **1369** | **Yes** | **1364** | **5** | | +| **Total** | **1376** | **Yes** | **1371** | **5** | | ### Bash Spec Tests Breakdown @@ -147,7 +147,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | nl.test.sh | 14 | line numbering | | nounset.test.sh | 7 | `set -u` unbound variable checks, `${var:-default}` nounset-aware | | paste.test.sh | 4 | line merging with `-s` serial and `-d` delimiter | -| path.test.sh | 14 | | +| path.test.sh | 18 | basename, dirname, `realpath` canonical path resolution | | pipes-redirects.test.sh | 19 | includes stderr redirects | | printf.test.sh | 32 | format specifiers, array expansion, `-v` variable assignment, `%q` shell quoting | | procsub.test.sh | 6 | | @@ -165,7 +165,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | eval-bugs.test.sh | 4 | regression tests for eval/script bugs | | script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes | | seq.test.sh | 12 | `seq` numeric sequences, `-w`, `-s`, decrement, negative | -| textrev.test.sh | 11 | `tac` reverse line order, `rev` reverse characters | +| textrev.test.sh | 14 | `tac` reverse line order, `rev` reverse characters, `yes` repeated output | | heredoc.test.sh | 10 | heredoc variable expansion, quoted delimiters, file redirects, `<<-` tab strip | | string-ops.test.sh | 14 | string replacement (prefix/suffix anchored), `${var:?}`, case conversion | | read-builtin.test.sh | 10 | `read` builtin, IFS splitting, `-r`, `-a` (array), `-n` (nchars), here-string |