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
35 changes: 35 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2959,6 +2959,41 @@ impl Interpreter {
return self.execute_getopts(&args, &command.redirects).await;
}

// Handle `caller` - needs direct access to call stack
if name == "caller" {
let frame_num: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
if self.call_stack.is_empty() {
// Outside any function
let mut result = ExecResult::err(String::new(), 1);
result = self.apply_redirections(result, &command.redirects).await?;
return Ok(result);
}
// caller 0 = immediate caller context
// call_stack includes current function; top-level is implicit
let source = "main";
let line = 1;
let output = if frame_num == 0 && self.call_stack.len() == 1 {
// Called from a function invoked at top level
format!("{} main {}\n", line, source)
} else if frame_num + 1 < self.call_stack.len() {
// Caller frame exists in stack
let idx = self.call_stack.len() - 2 - frame_num;
let frame = &self.call_stack[idx];
format!("{} {} {}\n", line, frame.name, source)
} else if frame_num + 1 == self.call_stack.len() {
// Frame is the top-level caller
format!("{} main {}\n", line, source)
} else {
// Frame out of range
let mut result = ExecResult::err(String::new(), 1);
result = self.apply_redirections(result, &command.redirects).await?;
return Ok(result);
};
let mut result = ExecResult::ok(output);
result = self.apply_redirections(result, &command.redirects).await?;
return Ok(result);
}

// Handle `mapfile`/`readarray` - needs direct access to arrays
if name == "mapfile" || name == "readarray" {
return self.execute_mapfile(&args, stdin.as_deref()).await;
Expand Down
36 changes: 36 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/functions.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,39 @@ echo "out: ${#FUNCNAME[@]}"
in: f
out: 0
### end

### func_caller_in_function
# caller reports calling context
### bash_diff
f() { caller 0; }
f
### expect
1 main main
### end

### func_caller_nested
# caller 0 reports immediate caller
### bash_diff
inner() { caller 0; }
outer() { inner; }
outer
### expect
1 outer main
### end

### func_caller_outside
# caller outside function returns error
caller 0
echo "exit:$?"
### expect
exit:1
### end

### func_caller_no_args
# caller with no args works same as caller 0
### bash_diff
f() { caller; }
f
### expect
1 main main
### end
8 changes: 4 additions & 4 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:** 1365 (1360 pass, 5 skip)
**Total spec test cases:** 1369 (1364 pass, 5 skip)

| Category | Cases | In CI | Pass | Skip | Notes |
|----------|-------|-------|------|------|-------|
| Bash (core) | 947 | Yes | 942 | 5 | `bash_spec_tests` in CI |
| Bash (core) | 951 | Yes | 946 | 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** | **1365** | **Yes** | **1360** | **5** | |
| **Total** | **1369** | **Yes** | **1364** | **5** | |

### Bash Spec Tests Breakdown

Expand All @@ -137,7 +137,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
| errexit.test.sh | 8 | set -e tests |
| fileops.test.sh | 28 | `mktemp`, `-d`, `-p`, template |
| find.test.sh | 10 | file search |
| functions.test.sh | 22 | local dynamic scoping, nested writes, FUNCNAME call stack |
| functions.test.sh | 26 | local dynamic scoping, nested writes, FUNCNAME call stack, `caller` builtin |
| getopts.test.sh | 9 | POSIX option parsing, combined flags, silent mode |
| globs.test.sh | 12 | for-loop glob expansion, recursive `**` |
| headtail.test.sh | 14 | |
Expand Down
Loading