1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

tee: should match GNU's output if used with /dev/full (#1944)

+ aligned 'tee' output with GNU tee when one of the files is '/dev/full'
+ don't stop tee when one of the outputs fails; just continue and return
error status from tee in the end

Co-authored-by: Ivan Rymarchyk <irymarchyk@arlo.com>
This commit is contained in:
Ivan 2021-03-27 22:02:49 +03:00 committed by GitHub
parent f66a188414
commit 500771c78d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 73 additions and 40 deletions

View file

@ -17,6 +17,7 @@ path = "src/tee.rs"
[dependencies] [dependencies]
clap = "2.33.3" clap = "2.33.3"
libc = "0.2.42" libc = "0.2.42"
retain_mut = "0.1.2"
uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["libc"] } uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["libc"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }

View file

@ -12,6 +12,7 @@ use clap::{App, Arg};
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::{copy, sink, stdin, stdout, Error, ErrorKind, Read, Result, Write}; use std::io::{copy, sink, stdin, stdout, Error, ErrorKind, Read, Result, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use retain_mut::RetainMut;
#[cfg(unix)] #[cfg(unix)]
use uucore::libc; use uucore::libc;
@ -93,18 +94,33 @@ fn tee(options: Options) -> Result<()> {
if options.ignore_interrupts { if options.ignore_interrupts {
ignore_interrupts()? ignore_interrupts()?
} }
let mut writers: Vec<Box<dyn Write>> = options let mut writers: Vec<NamedWriter> = options
.files .files
.clone() .clone()
.into_iter() .into_iter()
.map(|file| open(file, options.append)) .map(|file|
NamedWriter {
name: file.clone(),
inner: open(file, options.append),
}
)
.collect(); .collect();
writers.push(Box::new(stdout()));
let output = &mut MultiWriter { writers };
writers.insert(0, NamedWriter {
name: "'standard output'".to_owned(),
inner: Box::new(stdout()),
});
let mut output = MultiWriter::new(writers);
let input = &mut NamedReader { let input = &mut NamedReader {
inner: Box::new(stdin()) as Box<dyn Read>, inner: Box::new(stdin()) as Box<dyn Read>,
}; };
if copy(input, output).is_err() || output.flush().is_err() {
// TODO: replaced generic 'copy' call to be able to stop copying
// if all outputs are closed (due to errors)
if copy(input, &mut output).is_err() || output.flush().is_err() ||
output.error_occured() {
Err(Error::new(ErrorKind::Other, "")) Err(Error::new(ErrorKind::Other, ""))
} else { } else {
Ok(()) Ok(())
@ -112,7 +128,7 @@ fn tee(options: Options) -> Result<()> {
} }
fn open(name: String, append: bool) -> Box<dyn Write> { fn open(name: String, append: bool) -> Box<dyn Write> {
let path = PathBuf::from(name); let path = PathBuf::from(name.clone());
let inner: Box<dyn Write> = { let inner: Box<dyn Write> = {
let mut options = OpenOptions::new(); let mut options = OpenOptions::new();
let mode = if append { let mode = if append {
@ -125,55 +141,68 @@ fn open(name: String, append: bool) -> Box<dyn Write> {
Err(_) => Box::new(sink()), Err(_) => Box::new(sink()),
} }
}; };
Box::new(NamedWriter { inner, path }) as Box<dyn Write> Box::new(NamedWriter { inner, name }) as Box<dyn Write>
} }
struct MultiWriter { struct MultiWriter {
writers: Vec<Box<dyn Write>>, writers: Vec<NamedWriter>,
initial_len: usize,
}
impl MultiWriter {
fn new (writers: Vec<NamedWriter>) -> Self {
Self {
initial_len: writers.len(),
writers,
}
}
fn error_occured(&self) -> bool {
self.writers.len() != self.initial_len
}
} }
impl Write for MultiWriter { impl Write for MultiWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize> { fn write(&mut self, buf: &[u8]) -> Result<usize> {
for writer in &mut self.writers { self.writers.retain_mut(|writer| {
writer.write_all(buf)?; let result = writer.write_all(buf);
} match result {
Err(f) => {
show_info!("{}: {}", writer.name, f.to_string());
false
}
_ => true
}
});
Ok(buf.len()) Ok(buf.len())
} }
fn flush(&mut self) -> Result<()> { fn flush(&mut self) -> Result<()> {
for writer in &mut self.writers { self.writers.retain_mut(|writer| {
writer.flush()?; let result = writer.flush();
} match result {
Err(f) => {
show_info!("{}: {}", writer.name, f.to_string());
false
}
_ => true
}
});
Ok(()) Ok(())
} }
} }
struct NamedWriter { struct NamedWriter {
inner: Box<dyn Write>, inner: Box<dyn Write>,
path: PathBuf, pub name: String,
} }
impl Write for NamedWriter { impl Write for NamedWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize> { fn write(&mut self, buf: &[u8]) -> Result<usize> {
match self.inner.write(buf) { self.inner.write(buf)
Err(f) => {
self.inner = Box::new(sink()) as Box<dyn Write>;
show_warning!("{}: {}", self.path.display(), f.to_string());
Err(f)
}
okay => okay,
}
} }
fn flush(&mut self) -> Result<()> { fn flush(&mut self) -> Result<()> {
match self.inner.flush() { self.inner.flush()
Err(f) => {
self.inner = Box::new(sink()) as Box<dyn Write>;
show_warning!("{}: {}", self.path.display(), f.to_string());
Err(f)
}
okay => okay,
}
} }
} }
@ -185,7 +214,7 @@ impl Read for NamedReader {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> { fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self.inner.read(buf) { match self.inner.read(buf) {
Err(f) => { Err(f) => {
show_warning!("{}: {}", Path::new("stdin").display(), f.to_string()); show_info!("{}: {}", Path::new("stdin").display(), f.to_string());
Err(f) Err(f)
} }
okay => okay, okay => okay,

View file

@ -60,28 +60,31 @@ fn test_tee_append() {
#[test] #[test]
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn test_tee_no_more_writeable_stdout() { fn test_tee_no_more_writeable_1() {
let (_at, mut ucmd) = at_and_ucmd!(); // equals to 'tee /dev/full out2 <multi_read' call
let (at, mut ucmd) = at_and_ucmd!();
let content = (1..=10) let content = (1..=10)
.map(|x| format!("{}\n", x.to_string())) .map(|x| format!("{}\n", x.to_string()))
.collect::<String>(); .collect::<String>();
let file_out = "tee_file_out"; let file_out = "tee_file_out";
let _result = ucmd let result = ucmd
.arg("/dev/full") .arg("/dev/full")
.arg(file_out) .arg(file_out)
.pipe_in(&content[..]) .pipe_in(&content[..])
.fails(); .fails();
// TODO: comment in after https://github.com/uutils/coreutils/issues/1805 is fixed assert_eq!(at.read(file_out), content);
// assert_eq!(at.read(file_out), content); assert!(result.stdout.contains(&content));
// assert!(result.stdout.contains(&content)); assert!(result.stderr.contains("No space left on device"));
// assert!(result.stderr.contains("No space left on device"));
} }
#[test] #[test]
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn test_tee_no_more_writeable_stdin() { fn test_tee_no_more_writeable_2() {
// should be equals to 'tee out1 out2 >/dev/full <multi_read' call
// but currently there is no way to redirect stdout to /dev/full
// so this test is disabled
let (_at, mut ucmd) = at_and_ucmd!(); let (_at, mut ucmd) = at_and_ucmd!();
let _content = (1..=10) let _content = (1..=10)
.map(|x| format!("{}\n", x.to_string())) .map(|x| format!("{}\n", x.to_string()))