diff --git a/interpreter/src/vm/io.rs b/interpreter/src/vm/io.rs index 29022ef..352ec42 100644 --- a/interpreter/src/vm/io.rs +++ b/interpreter/src/vm/io.rs @@ -58,23 +58,37 @@ 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); - self.record.clear(); + // 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 // 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..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), @@ -106,7 +107,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..3003aab 100644 --- a/tests/by-util/test_awk.rs +++ b/tests/by-util/test_awk.rs @@ -908,3 +908,77 @@ 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 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 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() + .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"); + 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"); +}