Skip to content

Commit ccae9a0

Browse files
authored
fix(awk): accept expressions as printf format string (#829)
## Summary - Allow awk printf to accept any expression as format string, not just string literals - `printf substr($i,1,1)` now works instead of erroring with "printf requires format string" - Changed Printf action to store format as AwkExpr, evaluated at runtime ## Test plan - [x] `awk_printf_expression_format` — printf with substr expression - [x] `awk_printf_string_literal` — string literal format still works - [x] Full test suite passes Closes #810
1 parent e2c3181 commit ccae9a0

File tree

2 files changed

+40
-7
lines changed

2 files changed

+40
-7
lines changed

crates/bashkit/src/builtins/awk.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ enum AwkOutputTarget {
9696
#[derive(Debug, Clone)]
9797
enum AwkAction {
9898
Print(Vec<AwkExpr>, Option<AwkOutputTarget>),
99-
Printf(String, Vec<AwkExpr>, Option<AwkOutputTarget>),
99+
Printf(AwkExpr, Vec<AwkExpr>, Option<AwkOutputTarget>),
100100
Assign(String, AwkExpr),
101101
ArrayAssign(String, AwkExpr, AwkExpr), // arr[key] = val
102102
If(AwkExpr, Vec<AwkAction>, Vec<AwkAction>),
@@ -787,15 +787,22 @@ impl<'a> AwkParser<'a> {
787787
self.skip_whitespace();
788788
}
789789

790-
// Parse format string
791-
if self.pos >= self.input.len() || self.current_char().unwrap() != '"' {
790+
// Parse format string — accepts string literals or expressions
791+
if self.pos >= self.input.len() {
792792
self.in_print_context = false;
793793
return Err(Error::Execution(
794794
"awk: printf requires format string".to_string(),
795795
));
796796
}
797797

798-
let format = self.parse_string()?;
798+
let format_expr = if self.current_char().unwrap() == '"' {
799+
// String literal format — parse directly
800+
let s = self.parse_string()?;
801+
AwkExpr::String(s)
802+
} else {
803+
// Expression as format string (e.g., printf substr($1,1,1))
804+
self.parse_expression()?
805+
};
799806
let mut args = Vec::new();
800807

801808
self.skip_whitespace();
@@ -814,7 +821,7 @@ impl<'a> AwkParser<'a> {
814821
let target = self.parse_output_target()?;
815822
self.in_print_context = false;
816823

817-
Ok(AwkAction::Printf(format, args, target))
824+
Ok(AwkAction::Printf(format_expr, args, target))
818825
}
819826

820827
/// Parse optional output target after print/printf arguments: `> file`, `>> file`, `| cmd`.
@@ -2900,9 +2907,10 @@ impl AwkInterpreter {
29002907
self.write_output(&text, target);
29012908
AwkFlow::Continue
29022909
}
2903-
AwkAction::Printf(format, args, target) => {
2910+
AwkAction::Printf(format_expr, args, target) => {
2911+
let format_str = self.eval_expr(format_expr).as_string();
29042912
let values: Vec<AwkValue> = args.iter().map(|a| self.eval_expr(a)).collect();
2905-
let text = self.format_string(format, &values);
2913+
let text = self.format_string(&format_str, &values);
29062914
self.write_output(&text, target);
29072915
AwkFlow::Continue
29082916
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Test for awk printf accepting expressions as format string
2+
3+
use bashkit::Bash;
4+
5+
/// Issue #810: awk printf should accept expressions (not just string literals)
6+
#[tokio::test]
7+
async fn awk_printf_expression_format() {
8+
let mut bash = Bash::new();
9+
let result = bash
10+
.exec(r#"echo "my-project" | awk '{for(i=1;i<=NF;i++) printf substr($i,1,1)}'"#)
11+
.await
12+
.unwrap();
13+
assert_eq!(result.stdout.trim(), "m");
14+
}
15+
16+
/// Printf with string literal should still work
17+
#[tokio::test]
18+
async fn awk_printf_string_literal() {
19+
let mut bash = Bash::new();
20+
let result = bash
21+
.exec(r#"echo test | awk '{printf "%s\n", $1}'"#)
22+
.await
23+
.unwrap();
24+
assert_eq!(result.stdout.trim(), "test");
25+
}

0 commit comments

Comments
 (0)