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
29 changes: 29 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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::<Vec<_>>();
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() {
Expand Down
50 changes: 50 additions & 0 deletions crates/bashkit/tests/script_execution_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Loading