From 061376398bdbdc25289750306e5df3aecb47c99c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 16:40:06 +0000 Subject: [PATCH 1/2] fix(interpreter): split command substitution output on IFS in list context Unquoted $() results in for-loop word lists (and other list contexts) were treated as a single token. Now expand_word_to_fields performs IFS-based field splitting when the word is unquoted and contains a CommandSubstitution part, matching POSIX/bash semantics. Added spec tests: for-loop with find, space-separated words, newlines. Closes #347 --- crates/bashkit/src/interpreter/mod.rs | 40 ++++++++++++++++++- .../spec_cases/bash/command-subst.test.sh | 37 +++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 46a23fc4..a6b1baab 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -5288,9 +5288,45 @@ impl Interpreter { } } - // For other words, expand to a single field + // For other words, expand to a single field then apply IFS word splitting + // when the word is unquoted and contains a command substitution. + // Per POSIX, unquoted $() results undergo field splitting on IFS. let expanded = self.expand_word(word).await?; - Ok(vec![expanded]) + + let has_command_subst = !word.quoted + && word + .parts + .iter() + .any(|p| matches!(p, WordPart::CommandSubstitution(_))); + + if has_command_subst { + // Split on IFS characters (default: space, tab, newline). + // Consecutive IFS-whitespace characters are collapsed (no empty fields). + let ifs = self + .variables + .get("IFS") + .cloned() + .unwrap_or_else(|| " \t\n".to_string()); + + if ifs.is_empty() { + // Empty IFS: no splitting + return Ok(vec![expanded]); + } + + let fields: Vec = expanded + .split(|c: char| ifs.contains(c)) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if fields.is_empty() { + // All-whitespace expansion produces zero fields (elision) + return Ok(Vec::new()); + } + Ok(fields) + } else { + Ok(vec![expanded]) + } } /// Apply parameter expansion operator diff --git a/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh b/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh index 5d35cca3..b61f3468 100644 --- a/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh @@ -152,3 +152,40 @@ echo "$(echo "hello\"world")" ### expect hello"world ### end + +### subst_word_split_for_loop +# Command substitution output is word-split in for-loop list context +mkdir -p /src +echo "a" > /src/one.txt +echo "b" > /src/two.txt +echo "c" > /src/three.txt +count=0 +for f in $(find /src -name "*.txt" -type f | sort); do + count=$((count + 1)) +done +echo "$count" +### expect +3 +### end + +### subst_word_split_echo_multiword +# Command substitution producing space-separated words splits in for-loop +result="" +for w in $(echo "alpha beta gamma"); do + result="${result}[${w}]" +done +echo "$result" +### expect +[alpha][beta][gamma] +### end + +### subst_word_split_newlines +# Command substitution with newline-separated output splits on newlines +result="" +for line in $(printf 'x\ny\nz'); do + result="${result}(${line})" +done +echo "$result" +### expect +(x)(y)(z) +### end From 01ffa67bc13b12437d4a8a00cbeb40b422a6a930 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Feb 2026 22:43:01 +0000 Subject: [PATCH 2/2] fix(test): fix subst_word_split_for_loop bash comparison mismatch Replace filesystem-dependent find command with printf to avoid VFS vs real filesystem mismatch in CI comparison tests. The test still validates word-splitting of command substitution output in for-loop list context. --- crates/bashkit/tests/spec_cases/bash/command-subst.test.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh b/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh index b61f3468..38210a8d 100644 --- a/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh @@ -155,12 +155,8 @@ hello"world ### subst_word_split_for_loop # Command substitution output is word-split in for-loop list context -mkdir -p /src -echo "a" > /src/one.txt -echo "b" > /src/two.txt -echo "c" > /src/three.txt count=0 -for f in $(find /src -name "*.txt" -type f | sort); do +for f in $(printf '/src/one.txt\n/src/two.txt\n/src/three.txt\n'); do count=$((count + 1)) done echo "$count"