1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27: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]
clap = "2.33.3"
libc = "0.2.42"
retain_mut = "0.1.2"
uucore = { version=">=0.0.7", package="uucore", path="../../uucore", features=["libc"] }
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::io::{copy, sink, stdin, stdout, Error, ErrorKind, Read, Result, Write};
use std::path::{Path, PathBuf};
use retain_mut::RetainMut;
#[cfg(unix)]
use uucore::libc;
@ -93,18 +94,33 @@ fn tee(options: Options) -> Result<()> {
if options.ignore_interrupts {
ignore_interrupts()?
}
let mut writers: Vec<Box<dyn Write>> = options
let mut writers: Vec<NamedWriter> = options
.files
.clone()
.into_iter()
.map(|file| open(file, options.append))
.map(|file|
NamedWriter {
name: file.clone(),
inner: open(file, options.append),
}
)
.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 {
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, ""))
} else {
Ok(())
@ -112,7 +128,7 @@ fn tee(options: Options) -> Result<()> {
}
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 mut options = OpenOptions::new();
let mode = if append {
@ -125,55 +141,68 @@ fn open(name: String, append: bool) -> Box<dyn Write> {
Err(_) => Box::new(sink()),
}
};
Box::new(NamedWriter { inner, path }) as Box<dyn Write>
Box::new(NamedWriter { inner, name }) as Box<dyn Write>
}
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 {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
for writer in &mut self.writers {
writer.write_all(buf)?;
}
self.writers.retain_mut(|writer| {
let result = writer.write_all(buf);
match result {
Err(f) => {
show_info!("{}: {}", writer.name, f.to_string());
false
}
_ => true
}
});
Ok(buf.len())
}
fn flush(&mut self) -> Result<()> {
for writer in &mut self.writers {
writer.flush()?;
}
self.writers.retain_mut(|writer| {
let result = writer.flush();
match result {
Err(f) => {
show_info!("{}: {}", writer.name, f.to_string());
false
}
_ => true
}
});
Ok(())
}
}
struct NamedWriter {
inner: Box<dyn Write>,
path: PathBuf,
pub name: String,
}
impl Write for NamedWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
match 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,
}
self.inner.write(buf)
}
fn flush(&mut self) -> Result<()> {
match 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,
}
self.inner.flush()
}
}
@ -185,7 +214,7 @@ impl Read for NamedReader {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match self.inner.read(buf) {
Err(f) => {
show_warning!("{}: {}", Path::new("stdin").display(), f.to_string());
show_info!("{}: {}", Path::new("stdin").display(), f.to_string());
Err(f)
}
okay => okay,

View file

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