From c2cfc072cf64de3e684ebc44de1c8650eba56109 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Thu, 26 Mar 2026 13:53:41 +0000 Subject: [PATCH] fix(awk): accept expressions as printf format string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #810 — awk printf now accepts any expression as the format argument, not just string literals. `printf substr($i,1,1)` now works. Changed Printf action to store format as AwkExpr, evaluated at runtime. --- crates/bashkit/src/builtins/awk.rs | 22 +++++++++++------ crates/bashkit/tests/awk_printf_expr_test.rs | 25 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 crates/bashkit/tests/awk_printf_expr_test.rs diff --git a/crates/bashkit/src/builtins/awk.rs b/crates/bashkit/src/builtins/awk.rs index 49127fb5..29df664a 100644 --- a/crates/bashkit/src/builtins/awk.rs +++ b/crates/bashkit/src/builtins/awk.rs @@ -96,7 +96,7 @@ enum AwkOutputTarget { #[derive(Debug, Clone)] enum AwkAction { Print(Vec, Option), - Printf(String, Vec, Option), + Printf(AwkExpr, Vec, Option), Assign(String, AwkExpr), ArrayAssign(String, AwkExpr, AwkExpr), // arr[key] = val If(AwkExpr, Vec, Vec), @@ -787,15 +787,22 @@ impl<'a> AwkParser<'a> { self.skip_whitespace(); } - // Parse format string - if self.pos >= self.input.len() || self.current_char().unwrap() != '"' { + // Parse format string — accepts string literals or expressions + if self.pos >= self.input.len() { self.in_print_context = false; return Err(Error::Execution( "awk: printf requires format string".to_string(), )); } - let format = self.parse_string()?; + let format_expr = if self.current_char().unwrap() == '"' { + // String literal format — parse directly + let s = self.parse_string()?; + AwkExpr::String(s) + } else { + // Expression as format string (e.g., printf substr($1,1,1)) + self.parse_expression()? + }; let mut args = Vec::new(); self.skip_whitespace(); @@ -814,7 +821,7 @@ impl<'a> AwkParser<'a> { let target = self.parse_output_target()?; self.in_print_context = false; - Ok(AwkAction::Printf(format, args, target)) + Ok(AwkAction::Printf(format_expr, args, target)) } /// Parse optional output target after print/printf arguments: `> file`, `>> file`, `| cmd`. @@ -2900,9 +2907,10 @@ impl AwkInterpreter { self.write_output(&text, target); AwkFlow::Continue } - AwkAction::Printf(format, args, target) => { + AwkAction::Printf(format_expr, args, target) => { + let format_str = self.eval_expr(format_expr).as_string(); let values: Vec = args.iter().map(|a| self.eval_expr(a)).collect(); - let text = self.format_string(format, &values); + let text = self.format_string(&format_str, &values); self.write_output(&text, target); AwkFlow::Continue } diff --git a/crates/bashkit/tests/awk_printf_expr_test.rs b/crates/bashkit/tests/awk_printf_expr_test.rs new file mode 100644 index 00000000..b78eafce --- /dev/null +++ b/crates/bashkit/tests/awk_printf_expr_test.rs @@ -0,0 +1,25 @@ +//! Test for awk printf accepting expressions as format string + +use bashkit::Bash; + +/// Issue #810: awk printf should accept expressions (not just string literals) +#[tokio::test] +async fn awk_printf_expression_format() { + let mut bash = Bash::new(); + let result = bash + .exec(r#"echo "my-project" | awk '{for(i=1;i<=NF;i++) printf substr($i,1,1)}'"#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "m"); +} + +/// Printf with string literal should still work +#[tokio::test] +async fn awk_printf_string_literal() { + let mut bash = Bash::new(); + let result = bash + .exec(r#"echo test | awk '{printf "%s\n", $1}'"#) + .await + .unwrap(); + assert_eq!(result.stdout.trim(), "test"); +}