Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/bashkit/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ mod timeout;
mod vars;
mod wait;
mod wc;
mod yes;

#[cfg(feature = "git")]
mod git;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions crates/bashkit/src/builtins/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExecResult> {
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 {
Expand Down
37 changes: 37 additions & 0 deletions crates/bashkit/src/builtins/yes.rs
Original file line number Diff line number Diff line change
@@ -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<ExecResult> {
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))
}
}
2 changes: 2 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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));
Expand Down
32 changes: 32 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/path.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
25 changes: 25 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/textrev.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 5 additions & 5 deletions specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 | |
Expand All @@ -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 |
Expand Down
Loading