From ca812a7558fb6629e205cce3b175e2cbc9bddb3b Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Mon, 17 Jan 2022 10:52:15 -0500 Subject: [PATCH] tail: rm trailing \n if input doesn't end with one Fix a bug where `tail` would inappropriately add a newline to the last line of output even though the input did not end with one. --- src/uu/tail/src/lines.rs | 83 ++++++++++++++++++++++++++++++++++++++ src/uu/tail/src/tail.rs | 6 ++- tests/by-util/test_tail.rs | 5 +++ 3 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/uu/tail/src/lines.rs diff --git a/src/uu/tail/src/lines.rs b/src/uu/tail/src/lines.rs new file mode 100644 index 00000000000..6e472b32e06 --- /dev/null +++ b/src/uu/tail/src/lines.rs @@ -0,0 +1,83 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. +//! Iterate over lines, including the line ending character(s). +//! +//! This module provides the [`lines`] function, similar to the +//! [`BufRead::lines`] method. While the [`BufRead::lines`] method +//! yields [`String`] instances that do not include the line ending +//! characters (`"\n"` or `"\r\n"`), our function yields [`String`] +//! instances that include the line ending characters. This is useful +//! if the input data does not end with a newline character and you +//! want to preserve the exact form of the input data. +use std::io::BufRead; + +/// Returns an iterator over the lines, including line ending characters. +/// +/// This function is just like [`BufRead::lines`], but it includes the +/// line ending characters in each yielded [`String`] if the input +/// data has them. +/// +/// # Examples +/// +/// If the input data does not end with a newline character (`'\n'`), +/// then the last [`String`] yielded by this iterator also does not +/// end with a newline: +/// +/// ```rust,ignore +/// use std::io::BufRead; +/// use std::io::Cursor; +/// +/// let cursor = Cursor::new(b"x\ny\nz"); +/// let mut it = cursor.lines(); +/// +/// assert_eq!(it.next(), Some(String::from("x\n"))); +/// assert_eq!(it.next(), Some(String::from("y\n"))); +/// assert_eq!(it.next(), Some(String::from("z"))); +/// assert_eq!(it.next(), None); +/// ``` +pub(crate) fn lines(reader: B) -> Lines +where + B: BufRead, +{ + Lines { buf: reader } +} + +/// An iterator over the lines of an instance of `BufRead`. +/// +/// This struct is generally created by calling [`lines`] on a `BufRead`. +/// Please see the documentation of [`lines`] for more details. +pub(crate) struct Lines { + buf: B, +} + +impl Iterator for Lines { + type Item = std::io::Result; + + fn next(&mut self) -> Option> { + let mut buf = String::new(); + match self.buf.read_line(&mut buf) { + Ok(0) => None, + Ok(_n) => Some(Ok(buf)), + Err(e) => Some(Err(e)), + } + } +} + +#[cfg(test)] +mod tests { + use crate::lines::lines; + use std::io::Cursor; + + #[test] + fn test_lines() { + let cursor = Cursor::new(b"x\ny\nz"); + let mut it = lines(cursor).map(|l| l.unwrap()); + + assert_eq!(it.next(), Some(String::from("x\n"))); + assert_eq!(it.next(), Some(String::from("y\n"))); + assert_eq!(it.next(), Some(String::from("z"))); + assert_eq!(it.next(), None); + } +} diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 1dbdd389b5e..b10e30fb0a8 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -16,9 +16,11 @@ extern crate clap; extern crate uucore; mod chunks; +mod lines; mod parse; mod platform; use chunks::ReverseChunks; +use lines::lines; use clap::{App, Arg}; use std::collections::VecDeque; @@ -482,8 +484,8 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR // data in the ringbuf. match settings.mode { FilterMode::Lines(count, _) => { - for line in unbounded_tail_collect(reader.lines(), count, settings.beginning) { - println!("{}", line); + for line in unbounded_tail_collect(lines(reader), count, settings.beginning) { + print!("{}", line); } } FilterMode::Bytes(count) => { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 721c8a46777..e863e34b747 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -484,3 +484,8 @@ fn test_no_such_file() { .no_stdout() .stderr_contains("cannot open 'bogusfile' for reading: No such file or directory"); } + +#[test] +fn test_no_trailing_newline() { + new_ucmd!().pipe_in("x").succeeds().stdout_only("x"); +}