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]
|
[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" }
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue