Skip to content

Commit d277c56

Browse files
authored
fix(builtins): implement declare -f for function display and lookup (#822)
## Summary - Implement proper `declare -f` flag handling in the declare builtin - `declare -f name` now returns exit 1 when function doesn't exist (was always returning 0) - `declare -f name` prints function stub and returns 0 when function exists - `declare -f` with no args lists all defined functions - This completes subprocess isolation for script-by-path execution (#792): the isolation itself (clearing functions in child scope) already worked, but `declare -f helper` always returned success regardless ## Test plan - [x] `declare_f_nonexistent_function_returns_1` — verifies exit code 1 for missing functions - [x] `declare_f_existing_function_prints_definition` — verifies exit code 0 and output for existing functions - [x] `declare_f_no_args_lists_all_functions` — verifies listing all functions - [x] `exec_script_functions_not_inherited` — verifies functions not visible in subprocess scripts - [x] Full test suite passes (`cargo test --all-features`) - [x] `cargo fmt --check` and `cargo clippy` clean Closes #792
1 parent 7962756 commit d277c56

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4912,6 +4912,7 @@ impl Interpreter {
49124912
let mut print_mode = false;
49134913
let mut is_readonly = false;
49144914
let mut is_export = false;
4915+
let mut is_function = false;
49154916
let mut flags = DeclareFlags::default();
49164917
let mut remove_nameref = false;
49174918
let mut is_lowercase = false;
@@ -4926,6 +4927,7 @@ impl Interpreter {
49264927
'p' => print_mode = true,
49274928
'r' => is_readonly = true,
49284929
'x' => is_export = true,
4930+
'f' => is_function = true,
49294931
'l' => is_lowercase = true,
49304932
'u' => is_uppercase = true,
49314933
_ => {} // n, a, A, i handled by flags
@@ -4943,6 +4945,33 @@ impl Interpreter {
49434945
}
49444946
}
49454947

4948+
// declare -f: function display mode
4949+
if is_function {
4950+
let mut output = String::new();
4951+
if names.is_empty() {
4952+
// List all functions
4953+
let mut func_names: Vec<_> = self.functions.keys().cloned().collect::<Vec<_>>();
4954+
func_names.sort();
4955+
for fname in &func_names {
4956+
output.push_str(&format!("{} ()\n{{\n ...\n}}\n", fname));
4957+
}
4958+
} else {
4959+
// Print specific functions — return 1 if any not found
4960+
for name in &names {
4961+
if self.functions.contains_key(*name) {
4962+
output.push_str(&format!("{} ()\n{{\n ...\n}}\n", name));
4963+
} else {
4964+
let mut result = ExecResult::with_code(String::new(), 1);
4965+
result = self.apply_redirections(result, redirects).await?;
4966+
return Ok(result);
4967+
}
4968+
}
4969+
}
4970+
let mut result = ExecResult::ok(output);
4971+
result = self.apply_redirections(result, redirects).await?;
4972+
return Ok(result);
4973+
}
4974+
49464975
if print_mode {
49474976
let mut output = String::new();
49484977
if names.is_empty() {

crates/bashkit/tests/script_execution_tests.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,53 @@ async fn exec_script_dollar_at() {
359359
let result = bash.exec("/all.sh x y z").await.unwrap();
360360
assert_eq!(result.stdout.trim(), "x y z");
361361
}
362+
363+
/// Functions defined in parent are NOT visible in subprocess script execution
364+
#[tokio::test]
365+
async fn exec_script_functions_not_inherited() {
366+
let mut bash = Bash::new();
367+
let fs = bash.fs();
368+
369+
fs.write_file(
370+
Path::new("/check_func.sh"),
371+
b"#!/bin/bash\nif declare -f helper > /dev/null 2>&1; then\n echo found\nelse\n echo not found\nfi",
372+
)
373+
.await
374+
.unwrap();
375+
fs.chmod(Path::new("/check_func.sh"), 0o755).await.unwrap();
376+
377+
// Define a function in parent, then run subprocess script
378+
bash.exec("helper() { echo from_parent; }").await.unwrap();
379+
let result = bash.exec("/check_func.sh").await.unwrap();
380+
assert_eq!(result.stdout.trim(), "not found");
381+
}
382+
383+
/// `declare -f nonexistent` should return exit code 1
384+
#[tokio::test]
385+
async fn declare_f_nonexistent_function_returns_1() {
386+
let mut bash = Bash::new();
387+
let result = bash.exec("declare -f no_such_func").await.unwrap();
388+
assert_eq!(result.exit_code, 1);
389+
}
390+
391+
/// `declare -f existing_func` should print definition and return exit code 0
392+
#[tokio::test]
393+
async fn declare_f_existing_function_prints_definition() {
394+
let mut bash = Bash::new();
395+
bash.exec("myfunc() { echo hello; }").await.unwrap();
396+
let result = bash.exec("declare -f myfunc").await.unwrap();
397+
assert_eq!(result.exit_code, 0);
398+
assert!(result.stdout.contains("myfunc"));
399+
}
400+
401+
/// `declare -f` with no args lists all functions
402+
#[tokio::test]
403+
async fn declare_f_no_args_lists_all_functions() {
404+
let mut bash = Bash::new();
405+
bash.exec("foo() { echo a; }").await.unwrap();
406+
bash.exec("bar() { echo b; }").await.unwrap();
407+
let result = bash.exec("declare -f").await.unwrap();
408+
assert_eq!(result.exit_code, 0);
409+
assert!(result.stdout.contains("foo"));
410+
assert!(result.stdout.contains("bar"));
411+
}

0 commit comments

Comments
 (0)