Skip to content

Commit

Permalink
Implement tee -p and --output-error
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
eds-collabora committed Jun 23, 2022
1 parent 2fb743b commit 04002a5
Show file tree
Hide file tree
Showing 2 changed files with 512 additions and 10 deletions.
153 changes: 143 additions & 10 deletions src/uu/tee/src/tee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -27,13 +27,24 @@ 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)]
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]
Expand All @@ -47,6 +58,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) {
Expand Down Expand Up @@ -79,6 +109,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 behaviour (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 behaviour")
.conflicts_with(options::IGNORE_PIPE_ERRORS),
)
}

#[cfg(unix)]
Expand All @@ -96,10 +149,29 @@ 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()
Expand All @@ -118,7 +190,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>,
};
Expand Down Expand Up @@ -151,48 +223,109 @@ fn open(name: String, append: bool) -> 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(())
}
}
}

Expand Down
Loading

0 comments on commit 04002a5

Please sign in to comment.