diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 2bfa96fe..cae06153 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1327,20 +1327,59 @@ impl Interpreter { /// Execute a case statement async fn execute_case(&mut self, case_cmd: &CaseCommand) -> Result { + use crate::parser::CaseTerminator; let word_value = self.expand_word(&case_cmd.word).await?; - // Try each case item in order + let mut stdout = String::new(); + let mut stderr = String::new(); + let mut exit_code = 0; + let mut fallthrough = false; + for case_item in &case_cmd.cases { - for pattern in &case_item.patterns { - let pattern_str = self.expand_word(pattern).await?; - if self.pattern_matches(&word_value, &pattern_str) { - return self.execute_command_sequence(&case_item.commands).await; + let matched = if fallthrough { + true + } else { + let mut m = false; + for pattern in &case_item.patterns { + let pattern_str = self.expand_word(pattern).await?; + if self.pattern_matches(&word_value, &pattern_str) { + m = true; + break; + } + } + m + }; + + if matched { + let r = self.execute_command_sequence(&case_item.commands).await?; + stdout.push_str(&r.stdout); + stderr.push_str(&r.stderr); + exit_code = r.exit_code; + match case_item.terminator { + CaseTerminator::Break => { + return Ok(ExecResult { + stdout, + stderr, + exit_code, + control_flow: r.control_flow, + }); + } + CaseTerminator::FallThrough => { + fallthrough = true; + } + CaseTerminator::Continue => { + fallthrough = false; + } } } } - // No pattern matched - return success with no output - Ok(ExecResult::ok(String::new())) + Ok(ExecResult { + stdout, + stderr, + exit_code, + control_flow: ControlFlow::None, + }) } /// Execute a time command - measure wall-clock execution time diff --git a/crates/bashkit/src/parser/ast.rs b/crates/bashkit/src/parser/ast.rs index 85ef4ef5..6d187156 100644 --- a/crates/bashkit/src/parser/ast.rs +++ b/crates/bashkit/src/parser/ast.rs @@ -190,11 +190,23 @@ pub struct CaseCommand { pub span: Span, } +/// Terminator for a case item. +#[derive(Debug, Clone, PartialEq)] +pub enum CaseTerminator { + /// `;;` — stop matching + Break, + /// `;&` — fall through to next case body unconditionally + FallThrough, + /// `;;&` — continue checking remaining patterns + Continue, +} + /// A single case item. #[derive(Debug, Clone)] pub struct CaseItem { pub patterns: Vec, pub commands: Vec, + pub terminator: CaseTerminator, } /// Function definition. diff --git a/crates/bashkit/src/parser/lexer.rs b/crates/bashkit/src/parser/lexer.rs index ba159365..7e137104 100644 --- a/crates/bashkit/src/parser/lexer.rs +++ b/crates/bashkit/src/parser/lexer.rs @@ -77,7 +77,20 @@ impl<'a> Lexer<'a> { } ';' => { self.advance(); - Some(Token::Semicolon) + if self.peek_char() == Some(';') { + self.advance(); + if self.peek_char() == Some('&') { + self.advance(); + Some(Token::DoubleSemiAmp) // ;;& + } else { + Some(Token::DoubleSemicolon) // ;; + } + } else if self.peek_char() == Some('&') { + self.advance(); + Some(Token::SemiAmp) // ;& + } else { + Some(Token::Semicolon) + } } '|' => { self.advance(); diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 44aa298d..1986a514 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -945,12 +945,12 @@ impl<'a> Parser<'a> { self.skip_newlines()?; } - cases.push(CaseItem { patterns, commands }); - - // Consume ;; if present - if self.is_case_terminator() { - self.advance_double_semicolon(); - } + let terminator = self.parse_case_terminator(); + cases.push(CaseItem { + patterns, + commands, + terminator, + }); self.skip_newlines()?; } @@ -1000,18 +1000,30 @@ impl<'a> Parser<'a> { /// Check if current token is ;; (case terminator) fn is_case_terminator(&self) -> bool { - // The lexer returns Semicolon for ; but we need ;; - // For now, check for two semicolons - matches!(self.current_token, Some(tokens::Token::Semicolon)) + matches!( + self.current_token, + Some(tokens::Token::DoubleSemicolon) + | Some(tokens::Token::SemiAmp) + | Some(tokens::Token::DoubleSemiAmp) + ) } - /// Advance past ;; (double semicolon) - fn advance_double_semicolon(&mut self) { - if matches!(self.current_token, Some(tokens::Token::Semicolon)) { - self.advance(); - if matches!(self.current_token, Some(tokens::Token::Semicolon)) { + /// Parse case terminator: `;;` (break), `;&` (fallthrough), `;;&` (continue matching) + fn parse_case_terminator(&mut self) -> ast::CaseTerminator { + match self.current_token { + Some(tokens::Token::SemiAmp) => { + self.advance(); + ast::CaseTerminator::FallThrough + } + Some(tokens::Token::DoubleSemiAmp) => { + self.advance(); + ast::CaseTerminator::Continue + } + Some(tokens::Token::DoubleSemicolon) => { self.advance(); + ast::CaseTerminator::Break } + _ => ast::CaseTerminator::Break, } } diff --git a/crates/bashkit/src/parser/tokens.rs b/crates/bashkit/src/parser/tokens.rs index c8e38376..b22b3f0d 100644 --- a/crates/bashkit/src/parser/tokens.rs +++ b/crates/bashkit/src/parser/tokens.rs @@ -23,6 +23,15 @@ pub enum Token { /// Semicolon (;) Semicolon, + /// Double semicolon (;;) — case break + DoubleSemicolon, + + /// Case fallthrough (;&) + SemiAmp, + + /// Case continue-matching (;;&) + DoubleSemiAmp, + /// Pipe (|) Pipe, 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 da94d23c..e1858a9a 100644 --- a/crates/bashkit/tests/spec_cases/bash/control-flow.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/control-flow.test.sh @@ -173,6 +173,47 @@ case hello in hel*) echo prefix;; esac prefix ### end +### case_fallthrough +# Case ;& falls through to next body +case a in a) echo first ;& b) echo second ;; esac +### expect +first +second +### end + +### case_fallthrough_chain +# Case ;& chains through multiple bodies +case a in a) echo one ;& b) echo two ;& c) echo three ;; esac +### expect +one +two +three +### end + +### case_continue_matching +# Case ;;& continues checking remaining patterns +case "test" in t*) echo prefix ;;& *es*) echo middle ;; *z*) echo nope ;; esac +### expect +prefix +middle +### end + +### case_continue_no_match +# Case ;;& skips non-matching subsequent patterns +case "hello" in h*) echo first ;;& *z*) echo nope ;; *lo) echo last ;; esac +### expect +first +last +### end + +### case_fallthrough_no_match +# Case ;& falls through even if next pattern wouldn't match +case a in a) echo matched ;& z) echo fell_through ;; esac +### expect +matched +fell_through +### end + ### and_list_success # AND list with success true && echo yes diff --git a/specs/009-implementation-status.md b/specs/009-implementation-status.md index 9164a1ee..1f0e0645 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:** 1204 (1199 pass, 5 skip) +**Total spec test cases:** 1214 (1209 pass, 5 skip) | Category | Cases | In CI | Pass | Skip | Notes | |----------|-------|-------|------|------|-------| -| Bash (core) | 843 | Yes | 838 | 5 | `bash_spec_tests` in CI | +| Bash (core) | 853 | Yes | 848 | 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** | **1204** | **Yes** | **1199** | **5** | | +| **Total** | **1214** | **Yes** | **1209** | **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 | 22 | includes backtick substitution, nested quotes in `$()` (1 skipped) | -| control-flow.test.sh | 48 | if/elif/else, for, while, case, trap ERR, `[[ =~ ]]` BASH_REMATCH, compound input redirects | +| control-flow.test.sh | 53 | 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 |