mirror of
https://github.com/uutils/coreutils
synced 2024-11-16 09:48:03 +00:00
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.
This commit is contained in:
parent
13de6cabfc
commit
ca812a7558
3 changed files with 92 additions and 2 deletions
83
src/uu/tail/src/lines.rs
Normal file
83
src/uu/tail/src/lines.rs
Normal file
|
@ -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<B>(reader: B) -> Lines<B>
|
||||
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<B> {
|
||||
buf: B,
|
||||
}
|
||||
|
||||
impl<B: BufRead> Iterator for Lines<B> {
|
||||
type Item = std::io::Result<String>;
|
||||
|
||||
fn next(&mut self) -> Option<std::io::Result<String>> {
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<T: Read>(reader: &mut BufReader<T>, 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) => {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue