From 27761dadd4cfe2df43298c9a5aae74db8fff55bd Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:49:41 +0530 Subject: [PATCH 1/3] interpreter: count NR across files and keep the last record for END --- interpreter/src/vm/io.rs | 9 +++++++-- src/event.rs | 7 ++++++- tests/by-util/test_awk.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/interpreter/src/vm/io.rs b/interpreter/src/vm/io.rs index 29022ef..f908427 100644 --- a/interpreter/src/vm/io.rs +++ b/interpreter/src/vm/io.rs @@ -71,10 +71,15 @@ impl Interpreter<'_> { // TODO: consider caching the result of all this validation, so we can // directly dispatch to the correct functions and avoid extra writes to RT. // TODO: on Windows, we must strip `\r` depending on BINMODE. - pub fn read_record(&mut self, reader: impl BufRead) -> Result { + pub fn read_record(&mut self, mut reader: impl BufRead) -> Result { + // At EOF, keep NR, FNR and $0 of the last record for END + if reader.fill_buf()?.is_empty() { + return Ok(false); + } + // Update vars // TODO: optimize and make more ergonomic - self.symbols.nr = &self.symbols.fnr + &Value::new_int(1); + self.symbols.nr = &self.symbols.nr + &Value::new_int(1); self.symbols.fnr = &self.symbols.fnr + &Value::new_int(1); let rs = self.symbols.rs.clone(); diff --git a/src/event.rs b/src/event.rs index 6801847..cae79d8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -106,7 +106,12 @@ impl<'a> AwkRt<'a> { pub fn rule_event_loop(&mut self) -> Result<()> { let range = self.bc.rules_code(); - if range.is_empty() { + // Input is read unless the program has only BEGIN rules + if range.is_empty() + && self.bc.end_code().is_empty() + && self.bc.begin_file_code().is_empty() + && self.bc.end_file_code().is_empty() + { return Ok(()); } diff --git a/tests/by-util/test_awk.rs b/tests/by-util/test_awk.rs index 9571365..3d31a17 100644 --- a/tests/by-util/test_awk.rs +++ b/tests/by-util/test_awk.rs @@ -908,3 +908,32 @@ fn slash_after_special_variable_divides() { .succeeds() .stdout_only("0.5 2\n1 1\n2\n3\n"); } + +#[test] +fn end_sees_last_record() { + ucmd() + .arg(r"{ s += $2 } END { print NR, FNR, $0, s / NR }") + .pipe_in("a 1\nb 2\nc 6\n") + .succeeds() + .stdout_is("3 3 c 6 3\n"); +} + +#[test] +fn end_only_program_reads_input() { + ucmd() + .arg("END { print NR, $1 }") + .pipe_in("a\nb\n") + .succeeds() + .stdout_is("2 b\n"); +} + +#[test] +fn nr_counts_across_files() { + let ts = uutests::util::TestScenario::new("awk"); + ts.fixtures.write("a", "x\ny\n"); + ts.fixtures.write("b", "z\n"); + ts.cmd(crate::TESTS_BINARY) + .args(&["{ print NR, FNR }", "a", "b"]) + .succeeds() + .stdout_is("1 1\n2 2\n3 1\n"); +} From 5137bd0bd3cf69cc6ddbbe0ce86efa2511df31f8 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:53:06 +0530 Subject: [PATCH 2/3] interpreter: keep $0 across an empty last file for END --- interpreter/src/vm/io.rs | 1 - tests/by-util/test_awk.rs | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/interpreter/src/vm/io.rs b/interpreter/src/vm/io.rs index f908427..9880d86 100644 --- a/interpreter/src/vm/io.rs +++ b/interpreter/src/vm/io.rs @@ -65,7 +65,6 @@ impl Interpreter<'_> { self.symbols.filename = Value::new_string(name.into()); self.symbols.errno = Value::new_int(errno); self.symbols.fnr = Value::new_int(0); - self.record.clear(); } // TODO: consider caching the result of all this validation, so we can diff --git a/tests/by-util/test_awk.rs b/tests/by-util/test_awk.rs index 3d31a17..d9a3a54 100644 --- a/tests/by-util/test_awk.rs +++ b/tests/by-util/test_awk.rs @@ -927,6 +927,30 @@ fn end_only_program_reads_input() { .stdout_is("2 b\n"); } +#[test] +fn end_after_empty_file() { + let ts = uutests::util::TestScenario::new("awk"); + ts.fixtures.write("a", "x y\nlast z\n"); + ts.fixtures.write("empty", ""); + ts.cmd(crate::TESTS_BINARY) + .args(&["END { print NR, FNR, \"[\" $0 \"]\", NF }", "empty"]) + .succeeds() + .stdout_is("0 0 [] 0\n"); + ts.cmd(crate::TESTS_BINARY) + .args(&["END { print NR, FNR, \"[\" $0 \"]\", NF }", "a", "empty"]) + .succeeds() + .stdout_is("2 0 [last z] 2\n"); +} + +#[test] +fn end_after_missing_trailing_newline() { + ucmd() + .arg(r#"END { print NR, FNR, "[" $0 "]", NF }"#) + .pipe_in("p\nq r") + .succeeds() + .stdout_is("2 2 [q r] 2\n"); +} + #[test] fn nr_counts_across_files() { let ts = uutests::util::TestScenario::new("awk"); From c4decf251fd9c461e68a3c3d4ef06580f86e6019 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:09:51 +0530 Subject: [PATCH 3/3] interpreter: clear $0 before BEGINFILE, as gawk does BEGINFILE could read the last record of the previous file once the clear in begin_file_prelude was dropped. gawk clears $0 only when the program has a BEGINFILE rule, so END still keeps $0 across an empty last file when there is none. --- interpreter/src/vm/io.rs | 12 +++++++++++- src/event.rs | 5 +++-- tests/by-util/test_awk.rs | 21 +++++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/interpreter/src/vm/io.rs b/interpreter/src/vm/io.rs index 9880d86..352ec42 100644 --- a/interpreter/src/vm/io.rs +++ b/interpreter/src/vm/io.rs @@ -58,13 +58,23 @@ impl From<&[u8]> for FilePath { } impl Interpreter<'_> { - pub fn begin_file_prelude(&mut self, f: Option<&Path>, res: Option<&io::Error>) { + pub fn begin_file_prelude( + &mut self, + f: Option<&Path>, + res: Option<&io::Error>, + has_begin_file: bool, + ) { let name = f.map_or(b"-".as_slice(), |p| p.as_os_str().as_encoded_bytes()); let errno = res.map_or(0, |e| e.raw_os_error().unwrap_or(-1) as _); self.symbols.filename = Value::new_string(name.into()); self.symbols.errno = Value::new_int(errno); self.symbols.fnr = Value::new_int(0); + // Like gawk, BEGINFILE never sees the previous file's last record. + // Without BEGINFILE, $0 survives an empty last file for END. + if has_begin_file { + self.record.clear(); + } } // TODO: consider caching the result of all this validation, so we can diff --git a/src/event.rs b/src/event.rs index cae79d8..e9b38c0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -79,8 +79,9 @@ impl<'a> AwkRt<'a> { path: Option<&Path>, res: Option<&io::Error>, ) -> Result { - self.intrp.begin_file_prelude(path, res); - match self.drive(self.bc.begin_file_code())? { + let code = self.bc.begin_file_code(); + self.intrp.begin_file_prelude(path, res, !code.is_empty()); + match self.drive(code)? { CtrlSig::End => Ok(false), CtrlSig::NextFile => Ok(true), CtrlSig::Exit(code) => self.end_event_loop(code).map(|_| false), diff --git a/tests/by-util/test_awk.rs b/tests/by-util/test_awk.rs index d9a3a54..3003aab 100644 --- a/tests/by-util/test_awk.rs +++ b/tests/by-util/test_awk.rs @@ -942,6 +942,27 @@ fn end_after_empty_file() { .stdout_is("2 0 [last z] 2\n"); } +#[test] +fn beginfile_does_not_see_previous_record() { + let ts = uutests::util::TestScenario::new("awk"); + ts.fixtures.write("a", "x y\nlast z\n"); + ts.fixtures.write("b", "w\n"); + ts.fixtures.write("empty", ""); + ts.cmd(crate::TESTS_BINARY) + .args(&["BEGINFILE { print FILENAME, \"[\" $0 \"]\", NF }", "a", "b"]) + .succeeds() + .stdout_is("a [] 0\nb [] 0\n"); + // gawk clears $0 for BEGINFILE, so END after an empty last file sees it empty + ts.cmd(crate::TESTS_BINARY) + .args(&[ + "BEGINFILE { n++ } END { print n, \"[\" $0 \"]\", NF }", + "a", + "empty", + ]) + .succeeds() + .stdout_is("2 [] 0\n"); +} + #[test] fn end_after_missing_trailing_newline() { ucmd()