Skip to content

Commit 45ea773

Browse files
authored
fix(awk): evaluate regex literals against $0 in boolean context (#827)
## Summary - Fix awk compound patterns like `flag && /^id:/` which were ignoring the regex - Add `eval_expr_as_bool()` method that handles `AwkExpr::Regex` by matching against `$0` - Applied to `&&`, `||`, and `!` operators ## Test plan - [x] `awk_compound_pattern_and_regex` — `flag && /^id:/` correctly filters lines - [x] `test_awk_gsub_with_print` — regex args to gsub still work as pattern strings - [x] Full test suite passes Closes #808
1 parent b357e37 commit 45ea773

File tree

2 files changed

+67
-10
lines changed

2 files changed

+67
-10
lines changed

crates/bashkit/src/builtins/awk.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,6 +2057,19 @@ impl AwkInterpreter {
20572057
}
20582058
}
20592059

2060+
/// Evaluate an expression as a boolean, with special handling for regex
2061+
/// literals: `/regex/` is matched against $0 in boolean context (e.g. && / ||).
2062+
fn eval_expr_as_bool(&mut self, expr: &AwkExpr) -> bool {
2063+
if let AwkExpr::Regex(pattern) = expr {
2064+
let line = self.state.get_field(0).as_string();
2065+
if let Ok(re) = Regex::new(pattern) {
2066+
return re.is_match(&line);
2067+
}
2068+
return false;
2069+
}
2070+
self.eval_expr(expr).as_bool()
2071+
}
2072+
20602073
fn eval_expr(&mut self, expr: &AwkExpr) -> AwkValue {
20612074
match expr {
20622075
AwkExpr::Number(n) => AwkValue::Number(*n),
@@ -2112,8 +2125,16 @@ impl AwkInterpreter {
21122125
} else {
21132126
0.0
21142127
}),
2115-
"&&" => AwkValue::Number(if l.as_bool() && r.as_bool() { 1.0 } else { 0.0 }),
2116-
"||" => AwkValue::Number(if l.as_bool() || r.as_bool() { 1.0 } else { 0.0 }),
2128+
"&&" => {
2129+
let lb = self.eval_expr_as_bool(left);
2130+
let rb = self.eval_expr_as_bool(right);
2131+
AwkValue::Number(if lb && rb { 1.0 } else { 0.0 })
2132+
}
2133+
"||" => {
2134+
let lb = self.eval_expr_as_bool(left);
2135+
let rb = self.eval_expr_as_bool(right);
2136+
AwkValue::Number(if lb || rb { 1.0 } else { 0.0 })
2137+
}
21172138
"~" => {
21182139
if let Ok(re) = Regex::new(&r.as_string()) {
21192140
AwkValue::Number(if re.is_match(&l.as_string()) {
@@ -2143,14 +2164,17 @@ impl AwkInterpreter {
21432164
_ => AwkValue::Uninitialized,
21442165
}
21452166
}
2146-
AwkExpr::UnaryOp(op, expr) => {
2147-
let v = self.eval_expr(expr);
2148-
match op.as_str() {
2149-
"-" => AwkValue::Number(-v.as_number()),
2150-
"!" => AwkValue::Number(if v.as_bool() { 0.0 } else { 1.0 }),
2151-
_ => v,
2167+
AwkExpr::UnaryOp(op, expr) => match op.as_str() {
2168+
"-" => {
2169+
let v = self.eval_expr(expr);
2170+
AwkValue::Number(-v.as_number())
21522171
}
2153-
}
2172+
"!" => {
2173+
let b = self.eval_expr_as_bool(expr);
2174+
AwkValue::Number(if b { 0.0 } else { 1.0 })
2175+
}
2176+
_ => self.eval_expr(expr),
2177+
},
21542178
AwkExpr::Concat(parts) => {
21552179
let s: String = parts
21562180
.iter()
@@ -2231,7 +2255,12 @@ impl AwkInterpreter {
22312255
AwkValue::Number(if exists { 1.0 } else { 0.0 })
22322256
}
22332257
AwkExpr::FuncCall(name, args) => self.call_function(name, args),
2234-
AwkExpr::Regex(pattern) => AwkValue::String(pattern.clone()),
2258+
AwkExpr::Regex(pattern) => {
2259+
// When used as a standalone expression, /regex/ matches against $0.
2260+
// When used as a function argument (gsub, sub, match, split),
2261+
// it's evaluated as a string pattern, so return the pattern string.
2262+
AwkValue::String(pattern.clone())
2263+
}
22352264
AwkExpr::Match(expr, pattern) => {
22362265
let s = self.eval_expr(expr).as_string();
22372266
if let Ok(re) = Regex::new(pattern) {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Tests for awk compound patterns and related fixes
2+
3+
use bashkit::Bash;
4+
use std::path::Path;
5+
6+
/// Issue #808: awk compound pattern `expr && /regex/` should match correctly
7+
#[tokio::test]
8+
async fn awk_compound_pattern_and_regex() {
9+
let mut bash = Bash::new();
10+
let fs = bash.fs();
11+
fs.write_file(Path::new("/tmp/t.txt"), b"id: t1\nstatus: open\n")
12+
.await
13+
.unwrap();
14+
let result = bash
15+
.exec(
16+
r#"awk '
17+
BEGIN { FS=": "; flag=1 }
18+
flag && /^id:/ { print "id matched: " $0 }
19+
flag && /^status:/ { print "status matched: " $0 }
20+
' /tmp/t.txt"#,
21+
)
22+
.await
23+
.unwrap();
24+
let lines: Vec<&str> = result.stdout.trim().lines().collect();
25+
assert_eq!(lines.len(), 2);
26+
assert_eq!(lines[0], "id matched: id: t1");
27+
assert_eq!(lines[1], "status matched: status: open");
28+
}

0 commit comments

Comments
 (0)