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
13 changes: 9 additions & 4 deletions crates/bashkit/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,11 +826,14 @@ impl<'a> Parser<'a> {
let words = if self.is_keyword("in") {
self.advance(); // consume 'in'

// Parse word list until do/newline/;
// Parse word list until a list terminator (newline/;)
let mut words = Vec::new();
loop {
match &self.current_token {
Some(tokens::Token::Word(w)) if w == "do" => break,
// `do`/`done` are reserved words only in command position.
// Inside the `in` list they are ordinary words until a list
// terminator (`;`/newline), matching bash: `for a in do; do
// echo $a; done` iterates over the single word `do`.
Some(tokens::Token::Word(w))
| Some(tokens::Token::QuotedWord(w))
| Some(tokens::Token::QuotedGlobWord(w)) => {
Expand Down Expand Up @@ -929,11 +932,13 @@ impl<'a> Parser<'a> {
}
self.advance(); // consume 'in'

// Parse word list until do/newline/;
// Parse word list until a list terminator (newline/;)
let mut words = Vec::new();
loop {
match &self.current_token {
Some(tokens::Token::Word(w)) if w == "do" => break,
// `do`/`done` are reserved words only in command position.
// Inside the `in` list they are ordinary words until a list
// terminator (`;`/newline), matching bash.
Some(tokens::Token::Word(w))
| Some(tokens::Token::QuotedWord(w))
| Some(tokens::Token::QuotedGlobWord(w)) => {
Expand Down
39 changes: 39 additions & 0 deletions crates/bashkit/tests/integration/for_in_reserved_word_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! Regression: reserved words (`do`, `done`, `in`, `then`, ...) are ordinary
//! words inside a `for`/`select` `in` list until a list terminator (`;` or
//! newline). They only become keywords in command position.
//!
//! Found by the nightly differential proptest:
//! `for a in do; do echo $a; done` must print `do`, not nothing.

use bashkit::Bash;

#[tokio::test]
async fn for_in_do_word_iterates() {
let mut bash = Bash::new();
let result = bash.exec("for a in do; do echo $a; done").await.unwrap();
assert_eq!(result.stdout, "do\n");
}

#[tokio::test]
async fn for_in_multiple_reserved_words() {
let mut bash = Bash::new();
let result = bash
.exec("for a in do done then in; do echo $a; done")
.await
.unwrap();
assert_eq!(result.stdout, "do\ndone\nthen\nin\n");
}

#[tokio::test]
async fn for_in_reserved_word_newline_terminator() {
let mut bash = Bash::new();
let result = bash.exec("for a in done\ndo echo $a; done").await.unwrap();
assert_eq!(result.stdout, "done\n");
}

#[tokio::test]
async fn for_in_normal_words_still_work() {
let mut bash = Bash::new();
let result = bash.exec("for a in 1 2 3; do echo $a; done").await.unwrap();
assert_eq!(result.stdout, "1\n2\n3\n");
}
1 change: 1 addition & 0 deletions crates/bashkit/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod dev_null_tests;
pub mod exec_options_tests;
pub mod final_env_tests;
pub mod find_multi_path_tests;
pub mod for_in_reserved_word_tests;
pub mod git_advanced_tests;
pub mod git_inspection_tests;
pub mod git_integration_tests;
Expand Down
Loading