Implement tee -p and --output-error

This has the following behaviours. On Unix:

- The default is to exit on pipe errors, and warn on other errors.

- "--output-error=warn" means to warn on all errors

- "--output-error", "--output-error=warn-nopipe" and "-p" all mean
  that pipe errors are suppressed, all other errors warn.

- "--output-error=exit" means to warn and exit on all errors.

- "--output-error=exit-nopipe" means to suppress pipe errors, and to
  warn and exit on all other errors.

On non-Unix platforms, all pipe behaviours are ignored, so the default
is effectively "--output-error=warn" and "warn-nopipe" is identical.
The only meaningful option is "--output-error=exit" which is identical
to "--output-error=exit-nopipe" on these platforms.

Note that warnings give a non-zero exit code, but do not halt writing
to non-erroring targets.
This commit is contained in:
Ed Smith 2022-06-20 16:48:20 +01:00 committed by Sylvestre Ledru
parent 37b754f462
commit a360504574
2 changed files with 521 additions and 17 deletions

View file

@ -8,7 +8,7 @@
#[macro_use]
extern crate uucore;
use clap::{crate_version, Arg, Command};
use clap::{crate_version, Arg, Command, PossibleValue};
use retain_mut::RetainMut;
use std::fs::OpenOptions;
use std::io::{copy, sink, stdin, stdout, Error, ErrorKind, Read, Result, Write};
@ -17,6 +17,8 @@ use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::format_usage;
// spell-checker:ignore nopipe
#[cfg(unix)]
use uucore::libc;
@ -27,6 +29,8 @@ mod options {
pub const APPEND: &str = "append";
pub const IGNORE_INTERRUPTS: &str = "ignore-interrupts";
pub const FILE: &str = "file";
pub const IGNORE_PIPE_ERRORS: &str = "ignore-pipe-errors";
pub const OUTPUT_ERROR: &str = "output-error";
}
#[allow(dead_code)]
@ -34,6 +38,15 @@ struct Options {
append: bool,
ignore_interrupts: bool,
files: Vec<String>,
output_error: Option<OutputErrorMode>,
}
#[derive(Clone, Debug)]
enum OutputErrorMode {
Warn,
WarnNoPipe,
Exit,
ExitNoPipe,
}
#[uucore::main]
@ -47,6 +60,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.values_of(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default(),
output_error: {
if matches.is_present(options::IGNORE_PIPE_ERRORS) {
Some(OutputErrorMode::WarnNoPipe)
} else if matches.is_present(options::OUTPUT_ERROR) {
if let Some(v) = matches.value_of(options::OUTPUT_ERROR) {
match v {
"warn" => Some(OutputErrorMode::Warn),
"warn-nopipe" => Some(OutputErrorMode::WarnNoPipe),
"exit" => Some(OutputErrorMode::Exit),
"exit-nopipe" => Some(OutputErrorMode::ExitNoPipe),
_ => unreachable!(),
}
} else {
Some(OutputErrorMode::WarnNoPipe)
}
} else {
None
}
},
};
match tee(&options) {
@ -79,6 +111,29 @@ pub fn uu_app<'a>() -> Command<'a> {
.multiple_occurrences(true)
.value_hint(clap::ValueHint::FilePath),
)
.arg(
Arg::new(options::IGNORE_PIPE_ERRORS)
.short('p')
.help("set write error behavior (ignored on non-Unix platforms)"),
)
.arg(
Arg::new(options::OUTPUT_ERROR)
.long(options::OUTPUT_ERROR)
.require_equals(true)
.min_values(0)
.max_values(1)
.possible_values([
PossibleValue::new("warn")
.help("produce warnings for errors writing to any output"),
PossibleValue::new("warn-nopipe")
.help("produce warnings for errors that are not pipe errors (ignored on non-unix platforms)"),
PossibleValue::new("exit").help("exit on write errors to any output"),
PossibleValue::new("exit-nopipe")
.help("exit on write errors to any output that are not pipe errors (equivalent to exit on non-unix platforms)"),
])
.help("set write error behavior")
.conflicts_with(options::IGNORE_PIPE_ERRORS),
)
}
#[cfg(unix)]
@ -96,19 +151,40 @@ fn ignore_interrupts() -> Result<()> {
Ok(())
}
#[cfg(unix)]
fn enable_pipe_errors() -> Result<()> {
let ret = unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL) };
if ret == libc::SIG_ERR {
return Err(Error::new(ErrorKind::Other, ""));
}
Ok(())
}
#[cfg(not(unix))]
fn enable_pipe_errors() -> Result<()> {
// Do nothing.
Ok(())
}
fn tee(options: &Options) -> Result<()> {
if options.ignore_interrupts {
ignore_interrupts()?;
}
if options.output_error.is_none() {
enable_pipe_errors()?;
}
let mut writers: Vec<NamedWriter> = options
.files
.clone()
.into_iter()
.map(|file| NamedWriter {
name: file.clone(),
inner: open(file, options.append),
.map(|file| {
Ok(NamedWriter {
name: file.clone(),
inner: open(file, options.append, options.output_error.as_ref())?,
})
})
.collect();
.collect::<Result<Vec<NamedWriter>>>()?;
writers.insert(
0,
@ -118,7 +194,7 @@ fn tee(options: &Options) -> Result<()> {
},
);
let mut output = MultiWriter::new(writers);
let mut output = MultiWriter::new(writers, options.output_error.clone());
let input = &mut NamedReader {
inner: Box::new(stdin()) as Box<dyn Read>,
};
@ -132,7 +208,11 @@ fn tee(options: &Options) -> Result<()> {
}
}
fn open(name: String, append: bool) -> Box<dyn Write> {
fn open(
name: String,
append: bool,
output_error: Option<&OutputErrorMode>,
) -> Result<Box<dyn Write>> {
let path = PathBuf::from(name.clone());
let inner: Box<dyn Write> = {
let mut options = OpenOptions::new();
@ -143,56 +223,125 @@ fn open(name: String, append: bool) -> Box<dyn Write> {
};
match mode.write(true).create(true).open(path.as_path()) {
Ok(file) => Box::new(file),
Err(_) => Box::new(sink()),
Err(f) => {
show_error!("{}: {}", name.maybe_quote(), f);
match output_error {
Some(OutputErrorMode::Exit) | Some(OutputErrorMode::ExitNoPipe) => {
return Err(f)
}
_ => Box::new(sink()),
}
}
}
};
Box::new(NamedWriter { inner, name }) as Box<dyn Write>
Ok(Box::new(NamedWriter { inner, name }) as Box<dyn Write>)
}
struct MultiWriter {
writers: Vec<NamedWriter>,
initial_len: usize,
output_error_mode: Option<OutputErrorMode>,
ignored_errors: usize,
}
impl MultiWriter {
fn new(writers: Vec<NamedWriter>) -> Self {
fn new(writers: Vec<NamedWriter>, output_error_mode: Option<OutputErrorMode>) -> Self {
Self {
initial_len: writers.len(),
writers,
output_error_mode,
ignored_errors: 0,
}
}
fn error_occurred(&self) -> bool {
self.writers.len() != self.initial_len
self.ignored_errors != 0
}
}
fn process_error(
mode: Option<&OutputErrorMode>,
f: Error,
writer: &NamedWriter,
ignored_errors: &mut usize,
) -> Result<()> {
match mode {
Some(OutputErrorMode::Warn) => {
show_error!("{}: {}", writer.name.maybe_quote(), f);
*ignored_errors += 1;
Ok(())
}
Some(OutputErrorMode::WarnNoPipe) | None => {
if f.kind() != ErrorKind::BrokenPipe {
show_error!("{}: {}", writer.name.maybe_quote(), f);
*ignored_errors += 1;
}
Ok(())
}
Some(OutputErrorMode::Exit) => {
show_error!("{}: {}", writer.name.maybe_quote(), f);
Err(f)
}
Some(OutputErrorMode::ExitNoPipe) => {
if f.kind() != ErrorKind::BrokenPipe {
show_error!("{}: {}", writer.name.maybe_quote(), f);
Err(f)
} else {
Ok(())
}
}
}
}
impl Write for MultiWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
let mut aborted = None;
let mode = self.output_error_mode.clone();
let mut errors = 0;
RetainMut::retain_mut(&mut self.writers, |writer| {
let result = writer.write_all(buf);
match result {
Err(f) => {
show_error!("{}: {}", writer.name.maybe_quote(), f);
if let Err(e) = process_error(mode.as_ref(), f, writer, &mut errors) {
if aborted.is_none() {
aborted = Some(e);
}
}
false
}
_ => true,
}
});
Ok(buf.len())
self.ignored_errors += errors;
if let Some(e) = aborted {
Err(e)
} else {
Ok(buf.len())
}
}
fn flush(&mut self) -> Result<()> {
let mut aborted = None;
let mode = self.output_error_mode.clone();
let mut errors = 0;
RetainMut::retain_mut(&mut self.writers, |writer| {
let result = writer.flush();
match result {
Err(f) => {
show_error!("{}: {}", writer.name.maybe_quote(), f);
if let Err(e) = process_error(mode.as_ref(), f, writer, &mut errors) {
if aborted.is_none() {
aborted = Some(e);
}
}
false
}
_ => true,
}
});
Ok(())
self.ignored_errors += errors;
if let Some(e) = aborted {
Err(e)
} else {
Ok(())
}
}
}

View file

@ -4,6 +4,8 @@ use crate::common::util::*;
// inspired by:
// https://github.com/coreutils/coreutils/tests/misc/tee.sh
// spell-checker:ignore nopipe
#[test]
fn test_tee_processing_multiple_operands() {
// POSIX says: "Processing of at least 13 file operands shall be supported."
@ -98,3 +100,356 @@ fn test_tee_no_more_writeable_2() {
// assert_eq!(at.read(file_out_b), content);
// assert!(result.stderr.contains("No space left on device"));
}
#[cfg(target_os = "linux")]
mod linux_only {
use crate::common::util::*;
use std::fs::File;
use std::io::Write;
use std::process::Output;
use std::thread;
fn make_broken_pipe() -> File {
use libc::c_int;
use std::os::unix::io::FromRawFd;
let mut fds: [c_int; 2] = [0, 0];
if unsafe { libc::pipe(&mut fds as *mut c_int) } != 0 {
panic!("Failed to create pipe");
}
// Drop the read end of the pipe
let _ = unsafe { File::from_raw_fd(fds[0]) };
// Make the write end of the pipe into a Rust File
unsafe { File::from_raw_fd(fds[1]) }
}
fn run_tee(proc: &mut UCommand) -> (String, Output) {
let content = (1..=100000).map(|x| format!("{}\n", x)).collect::<String>();
let mut prog = proc.run_no_wait();
let mut stdin = prog
.stdin
.take()
.unwrap_or_else(|| panic!("Could not take child process stdin"));
let c = content.clone();
let thread = thread::spawn(move || {
let _ = stdin.write_all(c.as_bytes());
});
let output = prog.wait_with_output().unwrap();
thread.join().unwrap();
(content, output)
}
fn expect_success(output: &Output) {
assert!(
output.status.success(),
"Command was expected to succeed.\nstdout = {}\n stderr = {}",
std::str::from_utf8(&output.stdout).unwrap(),
std::str::from_utf8(&output.stderr).unwrap(),
);
assert!(
output.stderr.is_empty(),
"Unexpected data on stderr.\n stderr = {}",
std::str::from_utf8(&output.stderr).unwrap(),
);
}
fn expect_failure(output: &Output, message: &str) {
assert!(
!output.status.success(),
"Command was expected to fail.\nstdout = {}\n stderr = {}",
std::str::from_utf8(&output.stdout).unwrap(),
std::str::from_utf8(&output.stderr).unwrap(),
);
assert!(
std::str::from_utf8(&output.stderr)
.unwrap()
.contains(message),
"Expected to see error message fragment {} in stderr, but did not.\n stderr = {}",
message,
std::str::from_utf8(&output.stderr).unwrap(),
);
}
fn expect_silent_failure(output: &Output) {
assert!(
!output.status.success(),
"Command was expected to fail.\nstdout = {}\n stderr = {}",
std::str::from_utf8(&output.stdout).unwrap(),
std::str::from_utf8(&output.stderr).unwrap(),
);
assert!(
output.stderr.is_empty(),
"Unexpected data on stderr.\n stderr = {}",
std::str::from_utf8(&output.stderr).unwrap(),
);
}
fn expect_correct(name: &str, at: &AtPath, contents: &str) {
assert!(at.file_exists(name));
let compare = at.read(name);
assert_eq!(compare, contents);
}
fn expect_short(name: &str, at: &AtPath, contents: &str) {
assert!(at.file_exists(name));
let compare = at.read(name);
assert!(
compare.len() < contents.len(),
"Too many bytes ({}) written to {} (should be a short count from {})",
compare.len(),
name,
contents.len()
);
assert!(contents.starts_with(&compare),
"Expected truncated output to be a prefix of the correct output, but it isn't.\n Correct: {}\n Compare: {}",
contents,
compare);
}
#[test]
fn test_pipe_error_default() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd.arg(file_out_a).set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_silent_failure(&output);
expect_short(file_out_a, &at, content.as_str());
}
#[test]
fn test_pipe_error_warn_nopipe_1() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("-p")
.arg(file_out_a)
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_success(&output);
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_pipe_error_warn_nopipe_2() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error")
.arg(file_out_a)
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_success(&output);
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_pipe_error_warn_nopipe_3() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=warn-nopipe")
.arg(file_out_a)
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_success(&output);
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_pipe_error_warn() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=warn")
.arg(file_out_a)
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "Broken pipe");
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_pipe_error_exit() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=exit")
.arg(file_out_a)
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "Broken pipe");
expect_short(file_out_a, &at, content.as_str());
}
#[test]
fn test_pipe_error_exit_nopipe() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=exit-nopipe")
.arg(file_out_a)
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_success(&output);
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_default() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd.arg(file_out_a).arg("/dev/full");
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_warn_nopipe_1() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("-p")
.arg(file_out_a)
.arg("/dev/full")
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_warn_nopipe_2() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error")
.arg(file_out_a)
.arg("/dev/full")
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_warn_nopipe_3() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=warn-nopipe")
.arg(file_out_a)
.arg("/dev/full")
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_warn() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=warn")
.arg(file_out_a)
.arg("/dev/full")
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_correct(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_exit() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=exit")
.arg(file_out_a)
.arg("/dev/full")
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_short(file_out_a, &at, content.as_str());
}
#[test]
fn test_space_error_exit_nopipe() {
let (at, mut ucmd) = at_and_ucmd!();
let file_out_a = "tee_file_out_a";
let proc = ucmd
.arg("--output-error=exit-nopipe")
.arg(file_out_a)
.arg("/dev/full")
.set_stdout(make_broken_pipe());
let (content, output) = run_tee(proc);
expect_failure(&output, "No space left");
expect_short(file_out_a, &at, content.as_str());
}
}