Skip to content

Commit ac9a418

Browse files
committed
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
1 parent f60c32c commit ac9a418

File tree

7 files changed

+136
-6
lines changed

7 files changed

+136
-6
lines changed

crates/bashkit/src/builtins/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ mod timeout;
6363
mod vars;
6464
mod wait;
6565
mod wc;
66+
mod yes;
6667

6768
#[cfg(feature = "git")]
6869
mod git;
@@ -94,7 +95,7 @@ pub use ls::{Find, Ls, Rmdir};
9495
pub use navigation::{Cd, Pwd};
9596
pub use nl::Nl;
9697
pub use paste::Paste;
97-
pub use path::{Basename, Dirname};
98+
pub use path::{Basename, Dirname, Realpath};
9899
pub use pipeline::{Tee, Watch, Xargs};
99100
pub use printf::Printf;
100101
pub use read::Read;
@@ -111,6 +112,7 @@ pub use timeout::Timeout;
111112
pub use vars::{Eval, Local, Readonly, Set, Shift, Shopt, Times, Unset};
112113
pub use wait::Wait;
113114
pub use wc::Wc;
115+
pub use yes::Yes;
114116

115117
#[cfg(feature = "git")]
116118
pub use git::Git;

crates/bashkit/src/builtins/path.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,38 @@ impl Builtin for Dirname {
119119
}
120120
}
121121

122+
/// The realpath builtin - resolve absolute pathname.
123+
///
124+
/// Usage: realpath [PATH...]
125+
///
126+
/// Resolves `.` and `..` components and prints absolute canonical paths.
127+
/// In bashkit's virtual filesystem, symlink resolution is not performed.
128+
pub struct Realpath;
129+
130+
#[async_trait]
131+
impl Builtin for Realpath {
132+
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
133+
if ctx.args.is_empty() {
134+
return Ok(ExecResult::err(
135+
"realpath: missing operand\n".to_string(),
136+
1,
137+
));
138+
}
139+
140+
let mut output = String::new();
141+
for arg in ctx.args {
142+
if arg.starts_with('-') {
143+
continue; // skip flags like -e, -m, -s
144+
}
145+
let resolved = super::resolve_path(ctx.cwd, arg);
146+
output.push_str(&resolved.to_string_lossy());
147+
output.push('\n');
148+
}
149+
150+
Ok(ExecResult::ok(output))
151+
}
152+
}
153+
122154
#[cfg(test)]
123155
#[allow(clippy::unwrap_used)]
124156
mod tests {

crates/bashkit/src/builtins/yes.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! yes builtin - repeatedly output a line
2+
3+
use async_trait::async_trait;
4+
5+
use super::{Builtin, Context};
6+
use crate::error::Result;
7+
use crate::interpreter::ExecResult;
8+
9+
/// The yes builtin - output a string repeatedly.
10+
///
11+
/// Usage: yes [STRING]
12+
///
13+
/// Repeatedly outputs STRING (default: "y") followed by newline.
14+
/// In bashkit, output is limited to avoid infinite loops.
15+
pub struct Yes;
16+
17+
/// Maximum number of lines to output (safety limit)
18+
const MAX_LINES: usize = 10_000;
19+
20+
#[async_trait]
21+
impl Builtin for Yes {
22+
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
23+
let text = if ctx.args.is_empty() {
24+
"y".to_string()
25+
} else {
26+
ctx.args.join(" ")
27+
};
28+
29+
let mut output = String::new();
30+
for _ in 0..MAX_LINES {
31+
output.push_str(&text);
32+
output.push('\n');
33+
}
34+
35+
Ok(ExecResult::ok(output))
36+
}
37+
}

crates/bashkit/src/interpreter/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ impl Interpreter {
237237
builtins.insert("tail".to_string(), Box::new(builtins::Tail));
238238
builtins.insert("basename".to_string(), Box::new(builtins::Basename));
239239
builtins.insert("dirname".to_string(), Box::new(builtins::Dirname));
240+
builtins.insert("realpath".to_string(), Box::new(builtins::Realpath));
240241
builtins.insert("mkdir".to_string(), Box::new(builtins::Mkdir));
241242
builtins.insert("mktemp".to_string(), Box::new(builtins::Mktemp));
242243
builtins.insert("rm".to_string(), Box::new(builtins::Rm));
@@ -260,6 +261,7 @@ impl Interpreter {
260261
builtins.insert("seq".to_string(), Box::new(builtins::Seq));
261262
builtins.insert("tac".to_string(), Box::new(builtins::Tac));
262263
builtins.insert("rev".to_string(), Box::new(builtins::Rev));
264+
builtins.insert("yes".to_string(), Box::new(builtins::Yes));
263265
builtins.insert("sort".to_string(), Box::new(builtins::Sort));
264266
builtins.insert("uniq".to_string(), Box::new(builtins::Uniq));
265267
builtins.insert("cut".to_string(), Box::new(builtins::Cut));

crates/bashkit/tests/spec_cases/bash/path.test.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,35 @@ dirname file
9797
### expect
9898
.
9999
### end
100+
101+
### realpath_absolute
102+
# realpath resolves .. components
103+
### bash_diff
104+
realpath /tmp/../tmp/test
105+
### expect
106+
/tmp/test
107+
### end
108+
109+
### realpath_dot
110+
# realpath resolves . components
111+
### bash_diff
112+
realpath /home/user/./file.txt
113+
### expect
114+
/home/user/file.txt
115+
### end
116+
117+
### realpath_dotdot
118+
# realpath resolves parent directory references
119+
### bash_diff
120+
realpath /home/user/docs/../file.txt
121+
### expect
122+
/home/user/file.txt
123+
### end
124+
125+
### realpath_no_args
126+
# realpath with no args should error
127+
realpath 2>/dev/null
128+
echo $?
129+
### expect
130+
1
131+
### end

crates/bashkit/tests/spec_cases/bash/textrev.test.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,28 @@ echo "a b c" | rev
9090
### expect
9191
c b a
9292
### end
93+
94+
### yes_default
95+
# yes outputs "y" by default (piped through head)
96+
yes | head -3
97+
### expect
98+
y
99+
y
100+
y
101+
### end
102+
103+
### yes_custom_string
104+
# yes with custom string
105+
yes hello | head -2
106+
### expect
107+
hello
108+
hello
109+
### end
110+
111+
### yes_multiple_args
112+
# yes joins multiple args with space
113+
yes a b c | head -2
114+
### expect
115+
a b c
116+
a b c
117+
### end

specs/009-implementation-status.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,17 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
103103

104104
## Spec Test Coverage
105105

106-
**Total spec test cases:** 1369 (1364 pass, 5 skip)
106+
**Total spec test cases:** 1376 (1371 pass, 5 skip)
107107

108108
| Category | Cases | In CI | Pass | Skip | Notes |
109109
|----------|-------|-------|------|------|-------|
110-
| Bash (core) | 951 | Yes | 946 | 5 | `bash_spec_tests` in CI |
110+
| Bash (core) | 958 | Yes | 953 | 5 | `bash_spec_tests` in CI |
111111
| AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g |
112112
| Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect |
113113
| Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E |
114114
| JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env |
115115
| Python | 57 | Yes | 57 | 0 | embedded Python (Monty) |
116-
| **Total** | **1369** | **Yes** | **1364** | **5** | |
116+
| **Total** | **1376** | **Yes** | **1371** | **5** | |
117117

118118
### Bash Spec Tests Breakdown
119119

@@ -147,7 +147,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
147147
| nl.test.sh | 14 | line numbering |
148148
| nounset.test.sh | 7 | `set -u` unbound variable checks, `${var:-default}` nounset-aware |
149149
| paste.test.sh | 4 | line merging with `-s` serial and `-d` delimiter |
150-
| path.test.sh | 14 | |
150+
| path.test.sh | 18 | basename, dirname, `realpath` canonical path resolution |
151151
| pipes-redirects.test.sh | 19 | includes stderr redirects |
152152
| printf.test.sh | 32 | format specifiers, array expansion, `-v` variable assignment, `%q` shell quoting |
153153
| procsub.test.sh | 6 | |
@@ -165,7 +165,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
165165
| eval-bugs.test.sh | 4 | regression tests for eval/script bugs |
166166
| script-exec.test.sh | 10 | script execution by path, $PATH search, exit codes |
167167
| seq.test.sh | 12 | `seq` numeric sequences, `-w`, `-s`, decrement, negative |
168-
| textrev.test.sh | 11 | `tac` reverse line order, `rev` reverse characters |
168+
| textrev.test.sh | 14 | `tac` reverse line order, `rev` reverse characters, `yes` repeated output |
169169
| heredoc.test.sh | 10 | heredoc variable expansion, quoted delimiters, file redirects, `<<-` tab strip |
170170
| string-ops.test.sh | 14 | string replacement (prefix/suffix anchored), `${var:?}`, case conversion |
171171
| read-builtin.test.sh | 10 | `read` builtin, IFS splitting, `-r`, `-a` (array), `-n` (nchars), here-string |

0 commit comments

Comments
 (0)