From a726a3f461690d85bfebfab91c5af794b2f27f6d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 19:22:07 +0000 Subject: [PATCH] fix(parser): expand variables in [[ =~ $var ]] regex patterns When regex pattern after =~ operator contains a variable reference ($), use parse_word() for proper expansion instead of collect_conditional_regex_pattern() which creates literal words. Literal regex patterns (without $) still use the special collector to preserve parens, backslashes, etc. Closes #400 https://claude.ai/code/session_01WZjYqxm5xMPAEe7FSHJkDy --- crates/bashkit/src/interpreter/mod.rs | 22 ++++++++++++++++++++++ crates/bashkit/src/parser/mod.rs | 16 +++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 81a534b3..f36b208f 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -9727,4 +9727,26 @@ echo "count=$COUNT" let result = bash.exec(r#"printf "a\nb\nc" | wc -l"#).await.unwrap(); assert_eq!(result.stdout.trim(), "2"); } + + #[tokio::test] + async fn test_regex_match_from_variable() { + // Issue #400: [[ =~ $var ]] should work with regex from variable + let mut bash = crate::Bash::new(); + let result = bash + .exec(r#"re="200"; line="hello 200 world"; [[ $line =~ $re ]] && echo "match" || echo "no""#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "match"); + } + + #[tokio::test] + async fn test_regex_match_literal() { + // Issue #400: literal regex should still work + let mut bash = crate::Bash::new(); + let result = bash + .exec(r#"line="hello 200 world"; [[ $line =~ 200 ]] && echo "match" || echo "no""#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "match"); + } } diff --git a/crates/bashkit/src/parser/mod.rs b/crates/bashkit/src/parser/mod.rs index 3a3bebcb..3270f0d6 100644 --- a/crates/bashkit/src/parser/mod.rs +++ b/crates/bashkit/src/parser/mod.rs @@ -1263,10 +1263,20 @@ impl<'a> Parser<'a> { let is_literal = matches!(self.current_token, Some(tokens::Token::LiteralWord(_))); - // After =~, collect the regex pattern (may contain parens) + // After =~, handle regex pattern. + // If the pattern contains $ (variable reference), parse it as a + // normal word so variables expand. Otherwise collect as literal + // regex to preserve parens, backslashes, etc. if saw_regex_op { - let pattern = self.collect_conditional_regex_pattern(&w_clone); - words.push(Word::literal(&pattern)); + if w_clone.contains('$') && !is_quoted { + // Variable reference — parse normally for expansion + let parsed = self.parse_word(w_clone); + words.push(parsed); + self.advance(); + } else { + let pattern = self.collect_conditional_regex_pattern(&w_clone); + words.push(Word::literal(&pattern)); + } saw_regex_op = false; continue; }