diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 46d1cc5e..93ae5581 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -1482,6 +1482,18 @@ impl<'a> Parser<'a> { pattern } + /// Check if current token starts with `=` (e.g., Word("=5") from `>=5`). + /// If so, return the rest of the word after `=`. + fn current_token_starts_with_eq(&self) -> Option { + match &self.current_token { + Some(tokens::Token::Assignment) => Some(String::new()), + Some(tokens::Token::Word(w)) | Some(tokens::Token::LiteralWord(w)) => { + w.strip_prefix('=').map(|rest| rest.to_string()) + } + _ => None, + } + } + fn parse_arithmetic_command(&mut self) -> Result { self.advance(); // consume '((' @@ -1531,12 +1543,30 @@ impl<'a> Parser<'a> { } // Handle operators that are normally special tokens but valid in arithmetic Some(tokens::Token::RedirectIn) => { - expr.push('<'); self.advance(); + // Check if next token starts with '=' to form '<=' + if let Some(rest) = self.current_token_starts_with_eq() { + expr.push_str("<="); + if !rest.is_empty() { + expr.push_str(&rest); + } + self.advance(); + } else { + expr.push('<'); + } } Some(tokens::Token::RedirectOut) => { - expr.push('>'); self.advance(); + // Check if next token starts with '=' to form '>=' + if let Some(rest) = self.current_token_starts_with_eq() { + expr.push_str(">="); + if !rest.is_empty() { + expr.push_str(&rest); + } + self.advance(); + } else { + expr.push('>'); + } } Some(tokens::Token::And) => { expr.push_str("&&"); @@ -1554,6 +1584,49 @@ impl<'a> Parser<'a> { expr.push('&'); self.advance(); } + Some(tokens::Token::Assignment) => { + expr.push('='); + self.advance(); + } + // In arithmetic context, N> is a number followed by >, not a fd redirect + Some(tokens::Token::RedirectFd(fd)) => { + let fd = *fd; + self.advance(); + if let Some(rest) = self.current_token_starts_with_eq() { + // N>= → number >= ... + expr.push_str(&format!("{}>=", fd)); + if !rest.is_empty() { + expr.push_str(&rest); + } + self.advance(); + } else { + expr.push_str(&format!("{}>", fd)); + } + } + Some(tokens::Token::RedirectFdAppend(fd)) => { + // N>> in arithmetic is N >> (right shift) + let fd = *fd; + expr.push_str(&format!("{}>>", fd)); + self.advance(); + } + Some(tokens::Token::RedirectFdIn(fd)) => { + let fd = *fd; + self.advance(); + if let Some(rest) = self.current_token_starts_with_eq() { + expr.push_str(&format!("{}<=", fd)); + if !rest.is_empty() { + expr.push_str(&rest); + } + self.advance(); + } else { + expr.push_str(&format!("{}<", fd)); + } + } + Some(tokens::Token::RedirectAppend) => { + // >> in arithmetic is right shift + expr.push_str(">>"); + self.advance(); + } None => { return Err(Error::parse( "unexpected end of input in arithmetic command".to_string(), diff --git a/crates/bashkit/tests/spec_cases/bash/arithmetic.test.sh b/crates/bashkit/tests/spec_cases/bash/arithmetic.test.sh index 9ad74ee0..151e1672 100644 --- a/crates/bashkit/tests/spec_cases/bash/arithmetic.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/arithmetic.test.sh @@ -406,6 +406,41 @@ echo $((5 >= 3)); echo $((5 >= 5)); echo $((4 >= 5)) 0 ### end +### arith_cmd_ge_false +# (( a >= b )) should be false when a < b +(( 3 >= 5 )); echo $? +### expect +1 +### end + +### arith_cmd_ge_true +# (( a >= b )) should be true when a > b +(( 5 >= 3 )); echo $? +### expect +0 +### end + +### arith_cmd_ge_equal +# (( a >= b )) should be true when a == b +(( 5 >= 5 )); echo $? +### expect +0 +### end + +### arith_cmd_ge_nospace +# (( a>=b )) without spaces +(( 3>=5 )); echo $? +### expect +1 +### end + +### arith_cmd_le_nospace +# (( a<=b )) without spaces +(( 5<=3 )); echo $? +### expect +1 +### end + ### let_basic # let evaluates arithmetic and assigns let x=5+3