Skip to content

Commit 335cb4f

Browse files
chaliyclaude
andauthored
fix(bash): input redirections on compound commands (#245)
## Summary - `while read ... done < file` and `done <<< string` and `done << EOF` now work - Parser's `parse_trailing_redirects` handles HereString/HereDoc/HereDocStrip tokens - Interpreter processes input redirects before compound execution, setting `pipeline_stdin` - Preserves existing pipe-based `pipeline_stdin` when no input redirects present ## Test plan - [x] 5 new tests: while read < file, herestring, heredoc, sum, for output redirect - [x] Existing pipe-to-while tests still pass (regression check) - [x] `cargo test --all-features` passes - [x] `cargo clippy` and `cargo fmt` clean Co-authored-by: Claude <noreply@anthropic.com>
1 parent e926822 commit 335cb4f

File tree

4 files changed

+117
-4
lines changed

4 files changed

+117
-4
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,19 @@ impl Interpreter {
571571
Command::Pipeline(pipeline) => self.execute_pipeline(pipeline).await,
572572
Command::List(list) => self.execute_list(list).await,
573573
Command::Compound(compound, redirects) => {
574+
// Process input redirections before executing compound
575+
let stdin = self.process_input_redirections(None, redirects).await?;
576+
let prev_pipeline_stdin = if stdin.is_some() {
577+
let prev = self.pipeline_stdin.take();
578+
self.pipeline_stdin = stdin;
579+
Some(prev)
580+
} else {
581+
None
582+
};
574583
let result = self.execute_compound(compound).await?;
584+
if let Some(prev) = prev_pipeline_stdin {
585+
self.pipeline_stdin = prev;
586+
}
575587
if redirects.is_empty() {
576588
Ok(result)
577589
} else {

crates/bashkit/src/parser/mod.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,59 @@ impl<'a> Parser<'a> {
415415
target: Word::literal(dst_fd.to_string()),
416416
});
417417
}
418+
Some(tokens::Token::HereString) => {
419+
self.advance();
420+
if let Ok(target) = self.expect_word() {
421+
redirects.push(Redirect {
422+
fd: None,
423+
kind: RedirectKind::HereString,
424+
target,
425+
});
426+
}
427+
}
428+
Some(tokens::Token::HereDoc) | Some(tokens::Token::HereDocStrip) => {
429+
let strip_tabs =
430+
matches!(self.current_token, Some(tokens::Token::HereDocStrip));
431+
self.advance();
432+
let (delimiter, quoted) = match &self.current_token {
433+
Some(tokens::Token::Word(w)) => (w.clone(), false),
434+
Some(tokens::Token::LiteralWord(w)) => (w.clone(), true),
435+
Some(tokens::Token::QuotedWord(w)) => (w.clone(), true),
436+
_ => break,
437+
};
438+
let content = self.lexer.read_heredoc(&delimiter);
439+
let content = if strip_tabs {
440+
let had_trailing_newline = content.ends_with('\n');
441+
let mut stripped: String = content
442+
.lines()
443+
.map(|l| l.trim_start_matches('\t'))
444+
.collect::<Vec<_>>()
445+
.join("\n");
446+
if had_trailing_newline {
447+
stripped.push('\n');
448+
}
449+
stripped
450+
} else {
451+
content
452+
};
453+
self.advance();
454+
let target = if quoted {
455+
Word::quoted_literal(content)
456+
} else {
457+
self.parse_word(content)
458+
};
459+
let kind = if strip_tabs {
460+
RedirectKind::HereDocStrip
461+
} else {
462+
RedirectKind::HereDoc
463+
};
464+
redirects.push(Redirect {
465+
fd: None,
466+
kind,
467+
target,
468+
});
469+
break; // heredoc consumes rest of input line
470+
}
418471
_ => break,
419472
}
420473
}

crates/bashkit/tests/spec_cases/bash/control-flow.test.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,54 @@ before: 2
329329
after: 0
330330
### end
331331

332+
### while_read_input_redirect
333+
# while read ... done < file
334+
printf "a\nb\nc\n" > /tmp/wr_test
335+
while read -r line; do echo "got: $line"; done < /tmp/wr_test
336+
### expect
337+
got: a
338+
got: b
339+
got: c
340+
### end
341+
342+
### while_read_herestring
343+
# while read ... done <<< string
344+
while read -r line; do echo "line: $line"; done <<< "hello"
345+
### expect
346+
line: hello
347+
### end
348+
349+
### while_read_heredoc
350+
# while read ... done << EOF
351+
while read -r line; do echo "$line"; done << EOF
352+
alpha
353+
beta
354+
EOF
355+
### expect
356+
alpha
357+
beta
358+
### end
359+
360+
### while_read_sum
361+
# Sum numbers from file redirect
362+
printf "10\n20\n30\n" > /tmp/wr_sum
363+
total=0
364+
while read -r n; do total=$((total + n)); done < /tmp/wr_sum
365+
echo $total
366+
### expect
367+
60
368+
### end
369+
370+
### for_output_redirect
371+
# for loop with output redirect
372+
for i in a b c; do echo $i; done > /tmp/for_out
373+
cat /tmp/for_out
374+
### expect
375+
a
376+
b
377+
c
378+
### end
379+
332380
### regex_match_in_conditional
333381
# Regex match used in && chain
334382
x="error: line 42"

specs/009-implementation-status.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,16 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
103103

104104
## Spec Test Coverage
105105

106-
**Total spec test cases:** 1181 (1176 pass, 5 skip)
106+
**Total spec test cases:** 1186 (1181 pass, 5 skip)
107107

108108
| Category | Cases | In CI | Pass | Skip | Notes |
109109
|----------|-------|-------|------|------|-------|
110-
| Bash (core) | 820 | Yes | 815 | 5 | `bash_spec_tests` in CI |
110+
| Bash (core) | 825 | Yes | 820 | 5 | `bash_spec_tests` in CI |
111111
| AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g |
112112
| Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect |
113113
| Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E |
114114
| JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env |
115-
| **Total** | **1181** | **Yes** | **1176** | **5** | |
115+
| **Total** | **1186** | **Yes** | **1181** | **5** | |
116116

117117
### Bash Spec Tests Breakdown
118118

@@ -128,7 +128,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See
128128
| command-not-found.test.sh | 17 | unknown command handling |
129129
| conditional.test.sh | 24 | `[[ ]]` conditionals, `=~` regex, BASH_REMATCH, glob `==`/`!=` |
130130
| command-subst.test.sh | 14 | includes backtick substitution (1 skipped) |
131-
| control-flow.test.sh | 43 | if/elif/else, for, while, case, trap ERR, `[[ =~ ]]` BASH_REMATCH |
131+
| control-flow.test.sh | 48 | if/elif/else, for, while, case, trap ERR, `[[ =~ ]]` BASH_REMATCH, compound input redirects |
132132
| cuttr.test.sh | 32 | cut and tr commands, `-z` zero-terminated |
133133
| date.test.sh | 38 | format specifiers, `-d` relative/compound/epoch, `-R`, `-I`, `%N` (2 skipped) |
134134
| diff.test.sh | 4 | line diffs |

0 commit comments

Comments
 (0)