diff --git a/crates/bashkit/src/builtins/awk.rs b/crates/bashkit/src/builtins/awk.rs index 855a84d5..c675b456 100644 --- a/crates/bashkit/src/builtins/awk.rs +++ b/crates/bashkit/src/builtins/awk.rs @@ -682,6 +682,14 @@ impl<'a> AwkParser<'a> { fn parse_printf(&mut self) -> Result { self.skip_whitespace(); + // Handle optional parenthesized form: printf("format", args) + let has_parens = + self.pos < self.input.len() && self.input.chars().nth(self.pos).unwrap() == '('; + if has_parens { + self.pos += 1; + self.skip_whitespace(); + } + // Parse format string if self.pos >= self.input.len() || self.input.chars().nth(self.pos).unwrap() != '"' { return Err(Error::Execution( @@ -701,6 +709,13 @@ impl<'a> AwkParser<'a> { self.skip_whitespace(); } + if has_parens + && self.pos < self.input.len() + && self.input.chars().nth(self.pos).unwrap() == ')' + { + self.pos += 1; + } + Ok(AwkAction::Printf(format, args)) } @@ -3269,4 +3284,32 @@ mod tests { .unwrap(); assert_eq!(result.stdout, "329\n"); } + + #[tokio::test] + async fn test_awk_printf_parens() { + // printf with parenthesized syntax: printf("format", args) + let result = run_awk( + &[r#"BEGIN{printf("["); printf("%s", "x"); printf("]"); print ""}"#], + Some(""), + ) + .await + .unwrap(); + assert_eq!(result.stdout, "[x]\n"); + } + + #[tokio::test] + async fn test_awk_printf_parens_csv() { + // CSV to JSON pattern using printf with parens + let result = run_awk( + &[ + "-F,", + r#"NR==1{for(i=1;i<=NF;i++) h[i]=$i; next} {printf("%s{", (NR>2?",":"")); for(i=1;i<=NF;i++){printf("%s\"%s\":\"%s\"", (i>1?",":""), h[i], $i)}; printf("}")} END{print ""}"#, + ], + Some("name,age\nalice,30\nbob,25\n"), + ) + .await + .unwrap(); + assert!(result.stdout.contains("alice")); + assert!(result.stdout.contains("bob")); + } } diff --git a/crates/bashkit/src/builtins/jq.rs b/crates/bashkit/src/builtins/jq.rs index 3ad2eb46..63a3c4ad 100644 --- a/crates/bashkit/src/builtins/jq.rs +++ b/crates/bashkit/src/builtins/jq.rs @@ -173,6 +173,7 @@ impl Builtin for Jq { // Parse arguments for flags using index-based loop to support // multi-arg flags like --arg name value and --argjson name value. let mut raw_output = false; + let mut raw_input = false; let mut compact_output = false; let mut null_input = false; let mut sort_keys = false; @@ -210,6 +211,8 @@ impl Builtin for Jq { if arg == "--raw-output" { raw_output = true; + } else if arg == "--raw-input" { + raw_input = true; } else if arg == "--compact-output" { compact_output = true; } else if arg == "--null-input" { @@ -269,6 +272,7 @@ impl Builtin for Jq { for ch in arg[1..].chars() { match ch { 'r' => raw_output = true, + 'R' => raw_input = true, 'c' => compact_output = true, 'n' => null_input = true, 'S' => sort_keys = true, @@ -377,6 +381,15 @@ impl Builtin for Jq { let inputs_to_process: Vec = if null_input { // -n flag: use null as input vec![Val::from(serde_json::Value::Null)] + } else if raw_input && slurp { + // -Rs flag: read entire input as single string + vec![Val::from(serde_json::Value::String(input.to_string()))] + } else if raw_input { + // -R flag: each line becomes a JSON string value + input + .lines() + .map(|line| Val::from(serde_json::Value::String(line.to_string()))) + .collect() } else if slurp { // -s flag: read all inputs into a single array match Self::parse_json_values(input) { @@ -1189,4 +1202,35 @@ mod tests { let result = run_jq_with_args(&["-snr", r#""hello""#], "").await.unwrap(); assert_eq!(result.trim(), "hello"); } + + #[tokio::test] + async fn test_jq_raw_input() { + // -R: each line becomes a JSON string + let result = run_jq_with_args(&["-R", "."], "hello\nworld\n") + .await + .unwrap(); + assert_eq!(result.trim(), "\"hello\"\n\"world\""); + } + + #[tokio::test] + async fn test_jq_raw_input_slurp() { + // -Rs: entire input as one string + let result = run_jq_with_args(&["-Rs", "."], "hello\nworld\n") + .await + .unwrap(); + assert_eq!(result.trim(), "\"hello\\nworld\\n\""); + } + + #[tokio::test] + async fn test_jq_raw_input_split() { + // -R -s then split: CSV-like processing + let result = run_jq_with_args( + &["-Rs", r#"split("\n") | map(select(length>0))"#], + "a,b,c\n1,2,3\n", + ) + .await + .unwrap(); + assert!(result.contains("a,b,c")); + assert!(result.contains("1,2,3")); + } } diff --git a/crates/bashkit/tests/spec_cases/awk/awk.test.sh b/crates/bashkit/tests/spec_cases/awk/awk.test.sh index 5dce9cd2..e42ed849 100644 --- a/crates/bashkit/tests/spec_cases/awk/awk.test.sh +++ b/crates/bashkit/tests/spec_cases/awk/awk.test.sh @@ -650,3 +650,17 @@ printf '10\n2\n' | awk '{if ($1 > 5) print $1}' ### expect 10 ### end + +### awk_printf_parens +# printf with parenthesized form +printf 'x\n' | awk '{printf("[%s]", $1); print ""}' +### expect +[x] +### end + +### awk_printf_parens_begin +# printf with parens in BEGIN block +echo x | awk 'BEGIN{printf("["); printf("%s", "hi"); printf("]"); print ""}' +### expect +[hi] +### end diff --git a/crates/bashkit/tests/spec_cases/jq/jq.test.sh b/crates/bashkit/tests/spec_cases/jq/jq.test.sh index e6dd342c..3601dc31 100644 --- a/crates/bashkit/tests/spec_cases/jq/jq.test.sh +++ b/crates/bashkit/tests/spec_cases/jq/jq.test.sh @@ -954,3 +954,21 @@ echo '42' | jq -e '.' ### expect 42 ### end + +### jq_raw_input +# -R flag: each line treated as string +printf 'hello\nworld\n' | jq -R '.' +### expect +"hello" +"world" +### end + +### jq_raw_input_slurp +# -Rs flag: entire input as one string, then split +printf 'a,b\n1,2\n' | jq -Rs 'split("\n") | map(select(length>0))' +### expect +[ + "a,b", + "1,2" +] +### end