Skip to content

Commit e1334ef

Browse files
chaliyclaude
andauthored
fix(interpreter): split command substitution output on IFS in list context (#374)
## Summary - Fix word splitting for unquoted `$()` command substitution in list contexts (e.g., `for x in $(cmd)`) - Previously, the entire command substitution output was treated as a single token; now it is split on IFS characters (space/tab/newline by default) - Add 3 spec tests covering for-loop with find, space-separated words, and newline-separated output ## Details The root cause was in `expand_word_to_fields()` in `crates/bashkit/src/interpreter/mod.rs`. For words that weren't array expansions, it called `expand_word()` which returns a single concatenated string, then wrapped it in a single-element vec. There was no IFS-based field splitting step for unquoted command substitutions. The fix checks if the word is unquoted and contains a `CommandSubstitution` part. If so, the expanded result is split on IFS characters (defaulting to space/tab/newline), with consecutive whitespace collapsed and empty results elided. This matches POSIX shell semantics and real bash behavior. ## Test plan - [x] Added 3 new spec tests in `command-subst.test.sh` (for-loop splitting, space splitting, newline splitting) - [x] All 1308 bash spec tests pass (1184 passed + 124 skipped, 0 failed) - [x] 1084/1084 bash comparison tests match real bash (100%) - [x] `cargo fmt --check` clean - [x] `cargo clippy --all-targets --all-features -- -D warnings` clean - [x] Unit tests and threat model tests pass Closes #347 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 76be440 commit e1334ef

2 files changed

Lines changed: 71 additions & 2 deletions

File tree

crates/bashkit/src/interpreter/mod.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5288,9 +5288,45 @@ impl Interpreter {
52885288
}
52895289
}
52905290

5291-
// For other words, expand to a single field
5291+
// For other words, expand to a single field then apply IFS word splitting
5292+
// when the word is unquoted and contains a command substitution.
5293+
// Per POSIX, unquoted $() results undergo field splitting on IFS.
52925294
let expanded = self.expand_word(word).await?;
5293-
Ok(vec![expanded])
5295+
5296+
let has_command_subst = !word.quoted
5297+
&& word
5298+
.parts
5299+
.iter()
5300+
.any(|p| matches!(p, WordPart::CommandSubstitution(_)));
5301+
5302+
if has_command_subst {
5303+
// Split on IFS characters (default: space, tab, newline).
5304+
// Consecutive IFS-whitespace characters are collapsed (no empty fields).
5305+
let ifs = self
5306+
.variables
5307+
.get("IFS")
5308+
.cloned()
5309+
.unwrap_or_else(|| " \t\n".to_string());
5310+
5311+
if ifs.is_empty() {
5312+
// Empty IFS: no splitting
5313+
return Ok(vec![expanded]);
5314+
}
5315+
5316+
let fields: Vec<String> = expanded
5317+
.split(|c: char| ifs.contains(c))
5318+
.filter(|s| !s.is_empty())
5319+
.map(|s| s.to_string())
5320+
.collect();
5321+
5322+
if fields.is_empty() {
5323+
// All-whitespace expansion produces zero fields (elision)
5324+
return Ok(Vec::new());
5325+
}
5326+
Ok(fields)
5327+
} else {
5328+
Ok(vec![expanded])
5329+
}
52945330
}
52955331

52965332
/// Apply parameter expansion operator

crates/bashkit/tests/spec_cases/bash/command-subst.test.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,36 @@ echo "$(echo "hello\"world")"
152152
### expect
153153
hello"world
154154
### end
155+
156+
### subst_word_split_for_loop
157+
# Command substitution output is word-split in for-loop list context
158+
count=0
159+
for f in $(printf '/src/one.txt\n/src/two.txt\n/src/three.txt\n'); do
160+
count=$((count + 1))
161+
done
162+
echo "$count"
163+
### expect
164+
3
165+
### end
166+
167+
### subst_word_split_echo_multiword
168+
# Command substitution producing space-separated words splits in for-loop
169+
result=""
170+
for w in $(echo "alpha beta gamma"); do
171+
result="${result}[${w}]"
172+
done
173+
echo "$result"
174+
### expect
175+
[alpha][beta][gamma]
176+
### end
177+
178+
### subst_word_split_newlines
179+
# Command substitution with newline-separated output splits on newlines
180+
result=""
181+
for line in $(printf 'x\ny\nz'); do
182+
result="${result}(${line})"
183+
done
184+
echo "$result"
185+
### expect
186+
(x)(y)(z)
187+
### end

0 commit comments

Comments
 (0)