From 4d696114505659d730b1d359b8a0ee09594b04a5 Mon Sep 17 00:00:00 2001 From: atharvamhaske Date: Sun, 8 Nov 2026 11:24:07 +0530 Subject: [PATCH] fix(gawk): exit a BEGIN-only awk program without reading input `awk 'BEGIN { print "AWK" }'` printed its output and then blocked reading stdin until EOF. Through `vm.process.exec` stdin stays open, so the call ran until its timeout and returned `timed_out` with exit 137. POSIX awk exits after a program made only of BEGIN actions without reading input. The wrapper now runs such a program with no input. awk-rs never feeds the main input to a plain `getline`, so this loses nothing. Named input files are not opened either, as with other awk versions. Fixes #2088 --- software/gawk/native/crates/awk/src/lib.rs | 36 ++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/software/gawk/native/crates/awk/src/lib.rs b/software/gawk/native/crates/awk/src/lib.rs index c334e8bd78..a76bfa34aa 100644 --- a/software/gawk/native/crates/awk/src/lib.rs +++ b/software/gawk/native/crates/awk/src/lib.rs @@ -5,6 +5,7 @@ use std::ffi::OsString; use std::io::{self, BufReader}; +use awk_rs::ast::{Pattern, Program}; use awk_rs::{Interpreter, Lexer, Parser}; /// Entry point for awk command. @@ -106,7 +107,11 @@ fn run_awk(args: &[String]) -> Result { let stdout = io::stdout(); let mut output = stdout.lock(); - let exit_code = if input_files.is_empty() { + let exit_code = if !reads_input(&program) { + interpreter + .run(Vec::::new(), &mut output) + .map_err(|e| format!("{}", e))? + } else if input_files.is_empty() { // Read from stdin let stdin = io::stdin(); let inputs = vec![BufReader::new(stdin.lock())]; @@ -139,6 +144,16 @@ fn run_awk(args: &[String]) -> Result { Ok(exit_code) } +/// Whether the program reads its input. POSIX awk exits after a program made only of BEGIN +/// actions without reading input, so stdin left open by the caller must not block it. awk-rs +/// never feeds the main input to a plain `getline`, so skipping the input loses nothing. +fn reads_input(program: &Program) -> bool { + !program + .rules + .iter() + .all(|rule| matches!(rule.pattern, Some(Pattern::Begin))) +} + fn variable_assignment(value: &str) -> Option<(&str, &str)> { let (name, value) = value.split_once('=')?; let mut chars = name.chars(); @@ -151,7 +166,24 @@ fn variable_assignment(value: &str) -> Option<(&str, &str)> { #[cfg(test)] mod tests { - use super::variable_assignment; + use super::{reads_input, variable_assignment}; + use awk_rs::{Lexer, Parser}; + + fn parse(source: &str) -> awk_rs::ast::Program { + let tokens = Lexer::new(source).tokenize().unwrap(); + Parser::new(tokens).parse().unwrap() + } + + #[test] + fn begin_only_programs_do_not_read_input() { + assert!(!reads_input(&parse(r#"BEGIN { print "AWK" }"#))); + assert!(!reads_input(&parse("BEGIN { x = 1 } BEGIN { print x }"))); + assert!(reads_input(&parse( + r#"BEGIN { print "AWK" } END { print NR }"# + ))); + assert!(reads_input(&parse("{ print $1 }"))); + assert!(reads_input(&parse("/x/"))); + } #[test] fn recognizes_operand_variable_assignments() {