Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions crates/bashkit/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.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,
}
}
Expand Down
48 changes: 48 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/control-flow.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions specs/009-implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |
Expand Down
Loading