diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index b246d5cc..9df4808e 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -4912,6 +4912,7 @@ impl Interpreter { let mut print_mode = false; let mut is_readonly = false; let mut is_export = false; + let mut is_function = false; let mut flags = DeclareFlags::default(); let mut remove_nameref = false; let mut is_lowercase = false; @@ -4926,6 +4927,7 @@ impl Interpreter { 'p' => print_mode = true, 'r' => is_readonly = true, 'x' => is_export = true, + 'f' => is_function = true, 'l' => is_lowercase = true, 'u' => is_uppercase = true, _ => {} // n, a, A, i handled by flags @@ -4943,6 +4945,33 @@ impl Interpreter { } } + // declare -f: function display mode + if is_function { + let mut output = String::new(); + if names.is_empty() { + // List all functions + let mut func_names: Vec<_> = self.functions.keys().cloned().collect::>(); + func_names.sort(); + for fname in &func_names { + output.push_str(&format!("{} ()\n{{\n ...\n}}\n", fname)); + } + } else { + // Print specific functions — return 1 if any not found + for name in &names { + if self.functions.contains_key(*name) { + output.push_str(&format!("{} ()\n{{\n ...\n}}\n", name)); + } else { + let mut result = ExecResult::with_code(String::new(), 1); + result = self.apply_redirections(result, redirects).await?; + return Ok(result); + } + } + } + let mut result = ExecResult::ok(output); + result = self.apply_redirections(result, redirects).await?; + return Ok(result); + } + if print_mode { let mut output = String::new(); if names.is_empty() { diff --git a/crates/bashkit/tests/script_execution_tests.rs b/crates/bashkit/tests/script_execution_tests.rs index 445dc98c..298dd8e2 100644 --- a/crates/bashkit/tests/script_execution_tests.rs +++ b/crates/bashkit/tests/script_execution_tests.rs @@ -359,3 +359,53 @@ async fn exec_script_dollar_at() { let result = bash.exec("/all.sh x y z").await.unwrap(); assert_eq!(result.stdout.trim(), "x y z"); } + +/// Functions defined in parent are NOT visible in subprocess script execution +#[tokio::test] +async fn exec_script_functions_not_inherited() { + let mut bash = Bash::new(); + let fs = bash.fs(); + + fs.write_file( + Path::new("/check_func.sh"), + b"#!/bin/bash\nif declare -f helper > /dev/null 2>&1; then\n echo found\nelse\n echo not found\nfi", + ) + .await + .unwrap(); + fs.chmod(Path::new("/check_func.sh"), 0o755).await.unwrap(); + + // Define a function in parent, then run subprocess script + bash.exec("helper() { echo from_parent; }").await.unwrap(); + let result = bash.exec("/check_func.sh").await.unwrap(); + assert_eq!(result.stdout.trim(), "not found"); +} + +/// `declare -f nonexistent` should return exit code 1 +#[tokio::test] +async fn declare_f_nonexistent_function_returns_1() { + let mut bash = Bash::new(); + let result = bash.exec("declare -f no_such_func").await.unwrap(); + assert_eq!(result.exit_code, 1); +} + +/// `declare -f existing_func` should print definition and return exit code 0 +#[tokio::test] +async fn declare_f_existing_function_prints_definition() { + let mut bash = Bash::new(); + bash.exec("myfunc() { echo hello; }").await.unwrap(); + let result = bash.exec("declare -f myfunc").await.unwrap(); + assert_eq!(result.exit_code, 0); + assert!(result.stdout.contains("myfunc")); +} + +/// `declare -f` with no args lists all functions +#[tokio::test] +async fn declare_f_no_args_lists_all_functions() { + let mut bash = Bash::new(); + bash.exec("foo() { echo a; }").await.unwrap(); + bash.exec("bar() { echo b; }").await.unwrap(); + let result = bash.exec("declare -f").await.unwrap(); + assert_eq!(result.exit_code, 0); + assert!(result.stdout.contains("foo")); + assert!(result.stdout.contains("bar")); +}