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:
parent
f66a188414
commit
500771c78d
3 changed files with 73 additions and 40 deletions
|
@ -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" }
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue