diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index bcd6cf7a..ddfc2add 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -571,7 +571,19 @@ impl Interpreter { Command::Pipeline(pipeline) => self.execute_pipeline(pipeline).await, Command::List(list) => self.execute_list(list).await, Command::Compound(compound, redirects) => { + // Process input redirections before executing compound + let stdin = self.process_input_redirections(None, redirects).await?; + let prev_pipeline_stdin = if stdin.is_some() { + let prev = self.pipeline_stdin.take(); + self.pipeline_stdin = stdin; + Some(prev) + } else { + None + }; let result = self.execute_compound(compound).await?; + if let Some(prev) = prev_pipeline_stdin { + self.pipeline_stdin = prev; + } if redirects.is_empty() { Ok(result) } else { diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 740e4228..44aa298d 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -415,6 +415,59 @@ impl<'a> Parser<'a> { target: Word::literal(dst_fd.to_string()), }); } + Some(tokens::Token::HereString) => { + self.advance(); + if let Ok(target) = self.expect_word() { + redirects.push(Redirect { + fd: None, + kind: RedirectKind::HereString, + target, + }); + } + } + Some(tokens::Token::HereDoc) | Some(tokens::Token::HereDocStrip) => { + let strip_tabs = + matches!(self.current_token, Some(tokens::Token::HereDocStrip)); + self.advance(); + let (delimiter, quoted) = match &self.current_token { + Some(tokens::Token::Word(w)) => (w.clone(), false), + Some(tokens::Token::LiteralWord(w)) => (w.clone(), true), + Some(tokens::Token::QuotedWord(w)) => (w.clone(), true), + _ => break, + }; + let content = self.lexer.read_heredoc(&delimiter); + let content = if strip_tabs { + let had_trailing_newline = content.ends_with('\n'); + let mut stripped: String = content + .lines() + .map(|l| l.trim_start_matches('\t')) + .collect::>() + .join("\n"); + if had_trailing_newline { + stripped.push('\n'); + } + stripped + } else { + content + }; + self.advance(); + let target = if quoted { + Word::quoted_literal(content) + } else { + self.parse_word(content) + }; + let kind = if strip_tabs { + RedirectKind::HereDocStrip + } else { + RedirectKind::HereDoc + }; + redirects.push(Redirect { + fd: None, + kind, + target, + }); + break; // heredoc consumes rest of input line + } _ => break, } } diff --git a/crates/bashkit/tests/spec_cases/bash/control-flow.test.sh b/crates/bashkit/tests/spec_cases/bash/control-flow.test.sh index 321f5b45..da94d23c 100644 --- a/crates/bashkit/tests/spec_cases/bash/control-flow.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/control-flow.test.sh @@ -329,6 +329,54 @@ before: 2 after: 0 ### end +### while_read_input_redirect +# while read ... done < file +printf "a\nb\nc\n" > /tmp/wr_test +while read -r line; do echo "got: $line"; done < /tmp/wr_test +### expect +got: a +got: b +got: c +### end + +### while_read_herestring +# while read ... done <<< string +while read -r line; do echo "line: $line"; done <<< "hello" +### expect +line: hello +### end + +### while_read_heredoc +# while read ... done << EOF +while read -r line; do echo "$line"; done << EOF +alpha +beta +EOF +### expect +alpha +beta +### end + +### while_read_sum +# Sum numbers from file redirect +printf "10\n20\n30\n" > /tmp/wr_sum +total=0 +while read -r n; do total=$((total + n)); done < /tmp/wr_sum +echo $total +### expect +60 +### end + +### for_output_redirect +# for loop with output redirect +for i in a b c; do echo $i; done > /tmp/for_out +cat /tmp/for_out +### expect +a +b +c +### end + ### regex_match_in_conditional # Regex match used in && chain x="error: line 42" diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index 2fe4d316..ee409f1e 100644 --- a/specs/009-implementation-status.md +++ b/specs/009-implementation-status.md @@ -103,16 +103,16 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See ## Spec Test Coverage -**Total spec test cases:** 1181 (1176 pass, 5 skip) +**Total spec test cases:** 1186 (1181 pass, 5 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 820 | Yes | 815 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 825 | Yes | 820 | 5 | `bash_spec_tests` in CI | | AWK | 96 | Yes | 96 | 0 | loops, arrays, -v, ternary, field assign, getline, %.6g | | Grep | 76 | Yes | 76 | 0 | -z, -r, -a, -b, -H, -h, -f, -P, --include, --exclude, binary detect | | Sed | 75 | Yes | 75 | 0 | hold space, change, regex ranges, -E | | JQ | 114 | Yes | 114 | 0 | reduce, walk, regex funcs, --arg/--argjson, combined flags, input/inputs, env | -| **Total** | **1181** | **Yes** | **1176** | **5** | | +| **Total** | **1186** | **Yes** | **1181** | **5** | | ### Bash Spec Tests Breakdown @@ -128,7 +128,7 @@ Bashkit implements IEEE 1003.1-2024 Shell Command Language. See | command-not-found.test.sh | 17 | unknown command handling | | conditional.test.sh | 24 | `[[ ]]` conditionals, `=~` regex, BASH_REMATCH, glob `==`/`!=` | | command-subst.test.sh | 14 | includes backtick substitution (1 skipped) | -| control-flow.test.sh | 43 | if/elif/else, for, while, case, trap ERR, `[[ =~ ]]` BASH_REMATCH | +| control-flow.test.sh | 48 | if/elif/else, for, while, case, trap ERR, `[[ =~ ]]` BASH_REMATCH, compound input redirects | | cuttr.test.sh | 32 | cut and tr commands, `-z` zero-terminated | | date.test.sh | 38 | format specifiers, `-d` relative/compound/epoch, `-R`, `-I`, `%N` (2 skipped) | | diff.test.sh | 4 | line diffs |