Skip to content
Merged
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
206 changes: 206 additions & 0 deletions base/sh/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
//! - `set` — set/unset shell options and positional parameters.
//! - `.` (dot/source) — execute commands from a file in the current
//! environment.
//! - `pwd` — print the current working directory.
//! - `clear` — clear the terminal screen.
//! - `help` — list builtins or show help for a specific builtin.

use crate::expand::Environment;
use crate::job::JobTable;
Expand All @@ -40,6 +43,9 @@ pub fn is_builtin(name: &str) -> bool {
| "jobs"
| "fg"
| "bg"
| "pwd"
| "clear"
| "help"
)
}

Expand Down Expand Up @@ -67,6 +73,9 @@ pub fn run_builtin(name: &str, args: &[String], env: &mut Environment) -> i32 {
"exec" => builtin_exec(args, env),
"set" => builtin_set(args, env),
"." => builtin_dot(args, env),
"pwd" => builtin_pwd(args, env),
"clear" => builtin_clear(),
"help" => builtin_help(args),
// Job-control builtins without a job table — this path should
// not normally be taken (exec.rs dispatches them via
// run_job_builtin instead), but handle gracefully.
Expand Down Expand Up @@ -1078,6 +1087,96 @@ fn builtin_bg(args: &[String], env: &mut Environment, jobs: &mut JobTable) -> i3
0
}

// ── pwd ───────────────────────────────────────────────────────────

/// Print the current working directory.
///
/// - `pwd` — print `$PWD`. If `$PWD` is unset, fall back to
/// `std::env::current_dir()`.
fn builtin_pwd(args: &[String], env: &mut Environment) -> i32 {
if !args.is_empty() {
eprintln!("sh: pwd: too many arguments");
return 2;
}
if let Some(pwd) = env.get("PWD") {
println!("{pwd}");
} else {
match std::env::current_dir() {
Ok(p) => println!("{}", p.display()),
Err(e) => {
eprintln!("sh: pwd: {e}");
return 1;
}
}
}
0
}

// ── clear ─────────────────────────────────────────────────────────

/// Clear the terminal screen.
///
/// Writes the ANSI escape sequence to clear the screen and move the
/// cursor to the home position. The kernel's VT100 parser handles
/// these sequences.
fn builtin_clear() -> i32 {
print!("\x1b[2J\x1b[H");
use std::io::Write;
if std::io::stdout().flush().is_err() {
eprintln!("sh: clear: write error");
return 1;
}
0
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ── help ──────────────────────────────────────────────────────────

/// Static table of (name, short_desc, long_desc) for each builtin.
const BUILTIN_HELP: &[(&str, &str, &str)] = &[
("cd", "Change the working directory.", "cd [DIR]\n cd — go to $HOME.\n cd - — go to $OLDPWD and print the new directory.\n cd DIR — go to DIR.\n Updates $OLDPWD and $PWD."),
("exit", "Exit the shell.", "exit [N]\n Exit the shell with status N (default: $?)."),
("export", "Mark variables for export.", "export [NAME[=VALUE] ...]\n export — list all exported variables.\n export NAME — mark NAME for export.\n export NAME=VAL — set NAME to VAL and mark for export."),
("unset", "Remove shell variables.", "unset [-fv] NAME ...\n -v Unset variables (default).\n -f Unset functions (not yet supported)."),
("echo", "Print arguments to stdout.", "echo [-n] [ARG ...]\n -n Suppress trailing newline."),
("test", "Evaluate conditional expressions.", "test EXPR\n Evaluate EXPR and return 0 (true) or 1 (false).\n Supports string, integer, and file tests."),
("[", "Evaluate conditional expressions.", "[ EXPR ]\n Same as test, but requires a closing ]."),
("read", "Read a line from stdin.", "read [VAR ...]\n read — read into $REPLY.\n read VAR — read entire line into VAR.\n read A B C — split on IFS; last var gets remainder."),
("exec", "Replace the shell with a command.", "exec [CMD [ARG ...]]\n Replace the shell process with CMD.\n With no CMD, redirections apply to the shell itself."),
("set", "Set shell options or positional parameters.", "set [OPTION | -- ARG ...]\n set — list all variables.\n set -e / +e — enable/disable errexit.\n set -x / +x — enable/disable xtrace.\n set -- A B — set positional parameters."),
(".", "Execute commands from a file.", ". FILE [ARG ...]\n Read and execute commands from FILE in the current\n shell environment."),
("jobs", "List active jobs.", "jobs\n Print one line per active job showing its number,\n status, and the original command string."),
("fg", "Bring a job to the foreground.", "fg [%N]\n fg — bring the current job to the foreground.\n fg %N — bring job N to the foreground."),
("bg", "Resume a stopped job in the background.", "bg [%N]\n bg — resume the current stopped job.\n bg %N — resume job N in the background."),
("pwd", "Print the current working directory.", "pwd\n Print the value of $PWD. If $PWD is unset, fall back\n to the OS current directory."),
("clear", "Clear the terminal screen.", "clear\n Write ANSI escape sequences to clear the screen and\n move the cursor to the home position."),
("help", "Display help for builtins.", "help [BUILTIN]\n help — list all builtins with short descriptions.\n help BUILTIN — show detailed help for BUILTIN."),
];

/// Display help for builtins.
///
/// - `help` — list all builtins with one-line descriptions.
/// - `help NAME` — show detailed help for a specific builtin.
fn builtin_help(args: &[String]) -> i32 {
if args.is_empty() {
println!("Shell builtins:");
for (name, short, _) in BUILTIN_HELP {
println!(" {name:10} {short}");
}
return 0;
}

let name = args[0].as_str();
for (n, _short, long) in BUILTIN_HELP {
if *n == name {
println!("{long}");
return 0;
}
}

eprintln!("sh: help: no help for `{name}'");
1
}

// ── Job spec parsing ──────────────────────────────────────────────

/// Parse a job specification like `%1` or `1` into a job ID.
Expand Down Expand Up @@ -1845,4 +1944,111 @@ mod tests {
let status = run_job_builtin("jobs", &args(&[]), &mut e, &mut jobs);
assert_eq!(status, 0);
}

// ── pwd ────────────────────────────────────────────────────────

#[test]
fn pwd_prints_env_pwd() {
let mut e = env();
e.set("PWD", "/home/user", None);
let status = builtin_pwd(&args(&[]), &mut e);
assert_eq!(status, 0);
}

#[test]
fn pwd_fallback_when_unset() {
let mut e = env();
// PWD is not set — should fall back to std::env::current_dir()
let status = builtin_pwd(&args(&[]), &mut e);
assert_eq!(status, 0);
}
Comment on lines +1950 to +1964
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

New builtin tests miss output-contract assertions.

These tests validate status codes but not the required observable output (pwd text, clear escape sequence, help listing format/content). That leaves regressions undetected in the core behavior promised by this PR.

Also applies to: 1973-1976, 1993-1996

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/sh/src/builtins.rs` around lines 1941 - 1955, The tests
pwd_prints_env_pwd and pwd_fallback_when_unset only assert exit status; update
them to also capture and assert the command output: use the same test harness
that runs builtin_pwd (via builtin_pwd, env(), args()) but capture stdout/stderr
and assert that pwd_prints_env_pwd yields the exact PWD string ("/home/user")
and that pwd_fallback_when_unset yields the current directory string
(std::env::current_dir() formatted as string). Apply the same pattern to the
other failing tests referenced (the clear/escape-sequence and help/listing tests
at the other locations) by asserting their stdout contains the expected escape
sequence or help text content/format rather than only checking status. Ensure
you reference and use builtin_pwd, env, args and the help/clear builtin test
functions when adding the output assertions.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred to future work -- the existing test patterns in this file (echo_basic, echo_no_args, etc.) also only assert status codes, not output content. Adding stdout capture infrastructure is out of scope for this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dburkart, understood — that's a fair call. The existing echo_basic and similar tests confirm the same status-only pattern, so keeping the new builtin tests consistent makes sense. Happy to revisit output-capture assertions in a follow-up once the infrastructure is in place.


✏️ Learnings added
Learnt from: dburkart
Repo: dburkart/vibix PR: 907
File: base/sh/src/builtins.rs:1950-1964
Timestamp: 2026-05-06T05:16:34.614Z
Learning: In `base/sh/src/builtins.rs`, the test module does not yet have stdout/stderr capture infrastructure. All existing builtin tests (echo_basic, echo_no_args, pwd_prints_env_pwd, clear_returns_zero, help_no_args_lists_all, etc.) only assert exit status codes, not output content. Adding output-content assertions is deferred to future work.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


#[test]
fn pwd_rejects_extra_args() {
let mut e = env();
e.set("PWD", "/home/user", None);
let status = builtin_pwd(&args(&["extra"]), &mut e);
assert_eq!(status, 2);
}

#[test]
fn is_builtin_recognizes_pwd() {
assert!(is_builtin("pwd"));
}

#[test]
fn dispatch_pwd() {
let mut e = env();
e.set("PWD", "/tmp", None);
let status = run_builtin("pwd", &args(&[]), &mut e);
assert_eq!(status, 0);
}

// ── clear ──────────────────────────────────────────────────────

#[test]
fn clear_returns_zero() {
let status = builtin_clear();
assert_eq!(status, 0);
}

#[test]
fn is_builtin_recognizes_clear() {
assert!(is_builtin("clear"));
}

#[test]
fn dispatch_clear() {
let mut e = env();
let status = run_builtin("clear", &args(&[]), &mut e);
assert_eq!(status, 0);
}

// ── help ───────────────────────────────────────────────────────

#[test]
fn help_no_args_lists_all() {
let status = builtin_help(&args(&[]));
assert_eq!(status, 0);
}

#[test]
fn help_known_builtin() {
let status = builtin_help(&args(&["cd"]));
assert_eq!(status, 0);
}

#[test]
fn help_unknown_builtin() {
let status = builtin_help(&args(&["nosuch"]));
assert_eq!(status, 1);
}

#[test]
fn help_each_builtin_has_entry() {
// Verify every builtin recognized by is_builtin has a help entry.
let known = [
"cd", "exit", "export", "unset", "echo", "test", "[", "read",
"exec", "set", ".", "jobs", "fg", "bg", "pwd", "clear", "help",
];
for name in &known {
assert_eq!(
builtin_help(&args(&[name])),
0,
"missing help entry for `{name}`"
);
}
}

#[test]
fn is_builtin_recognizes_help() {
assert!(is_builtin("help"));
}

#[test]
fn dispatch_help() {
let mut e = env();
let status = run_builtin("help", &args(&[]), &mut e);
assert_eq!(status, 0);
}
}
Loading