Skip to content

Commit a927695

Browse files
authored
fix(interpreter): populate BASH_SOURCE[0] for PATH-resolved scripts (#1087)
## Summary - When a script is executed via PATH lookup (by name, not by path), `BASH_SOURCE[0]` was empty because only the bare command name was passed to `execute_script_content()` instead of the resolved path - Fixed `try_execute_script_via_path_search()` to pass the fully resolved `candidate` path - Added 2 new tests: direct BASH_SOURCE check and source-guard pattern via PATH lookup ## What changed In `try_execute_script_via_path_search()`, the code already resolves the full path (`candidate = PathBuf::from(dir).join(name)`) but was discarding it when calling `execute_script_content(name, ...)`. Now passes `&candidate.to_string_lossy()` instead. ## Test plan - [x] `bash_source_set_via_path_lookup` — verifies BASH_SOURCE[0] equals `/scripts/test.sh` when run by name via PATH - [x] `bash_source_guard_via_path_lookup` — verifies source-guard pattern (`BASH_SOURCE[0] == $0`) works correctly via PATH - [x] All existing bash_source tests still pass (6/6) - [x] Full test suite green (2700+ tests) - [x] `cargo fmt --check` clean - [x] `cargo clippy -- -D warnings` clean Closes #1085
1 parent 0f63b7b commit a927695

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

crates/bashkit/src/interpreter/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4151,8 +4151,9 @@ impl Interpreter {
41514151
}
41524152
if let Ok(content) = self.fs.read_file(&candidate).await {
41534153
let script_text = bytes_to_latin1_string(&content);
4154+
let resolved = candidate.to_string_lossy();
41544155
let result = self
4155-
.execute_script_content(name, &script_text, args, stdin, redirects)
4156+
.execute_script_content(&resolved, &script_text, args, stdin, redirects)
41564157
.await?;
41574158
return Ok(Some(result));
41584159
}

crates/bashkit/tests/bash_source_tests.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,54 @@ async fn bash_source_guard_direct_execution() {
5050
assert_eq!(result.stdout.trim(), "direct");
5151
}
5252

53+
/// BASH_SOURCE[0] is set to the resolved path when script is executed via PATH lookup
54+
#[tokio::test]
55+
async fn bash_source_set_via_path_lookup() {
56+
let mut bash = Bash::new();
57+
let fs = bash.fs();
58+
59+
// Create a script in /scripts
60+
fs.mkdir(Path::new("/scripts"), false).await.unwrap();
61+
fs.write_file(
62+
Path::new("/scripts/test.sh"),
63+
b"#!/bin/bash\necho \"source=${BASH_SOURCE[0]}\"",
64+
)
65+
.await
66+
.unwrap();
67+
fs.chmod(Path::new("/scripts/test.sh"), 0o755)
68+
.await
69+
.unwrap();
70+
71+
// Add /scripts to PATH and run by name
72+
let result = bash
73+
.exec("export PATH=\"/scripts:${PATH}\"\ntest.sh")
74+
.await
75+
.unwrap();
76+
assert_eq!(result.stdout.trim(), "source=/scripts/test.sh");
77+
}
78+
79+
/// Source guard pattern works correctly when script is executed via PATH lookup
80+
#[tokio::test]
81+
async fn bash_source_guard_via_path_lookup() {
82+
let mut bash = Bash::new();
83+
let fs = bash.fs();
84+
85+
fs.mkdir(Path::new("/bin2"), false).await.unwrap();
86+
fs.write_file(
87+
Path::new("/bin2/guard.sh"),
88+
b"#!/bin/bash\nif [[ \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then echo direct; else echo sourced; fi",
89+
)
90+
.await
91+
.unwrap();
92+
fs.chmod(Path::new("/bin2/guard.sh"), 0o755).await.unwrap();
93+
94+
let result = bash
95+
.exec("export PATH=\"/bin2:${PATH}\"\nguard.sh")
96+
.await
97+
.unwrap();
98+
assert_eq!(result.stdout.trim(), "direct");
99+
}
100+
53101
/// Source guard pattern: BASH_SOURCE[0] != $0 when sourced
54102
#[tokio::test]
55103
async fn bash_source_guard_sourced() {

0 commit comments

Comments
 (0)