jqrutils 0.1.0

Small Unix utilities: quiet, errfail, prefix
use std::env;
use std::io::{self, Read, Write};
use std::process::{Command, ExitCode, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

#[derive(Copy, Clone)]
enum Output {
    Stdout,
    Stderr,
}

struct State {
    streaming: bool,
    buffer: Vec<(Output, Vec<u8>)>,
}

impl State {
    fn flush(&mut self) {
        for (output, data) in self.buffer.drain(..) {
            let _ = match output {
                Output::Stdout => io::stdout().write_all(&data),
                Output::Stderr => io::stderr().write_all(&data),
            };
        }
        self.streaming = true;
    }
}

fn read_pipe(mut pipe: impl Read, output: Output, state: &Mutex<State>) {
    let mut buf = [0u8; 8192];
    loop {
        match pipe.read(&mut buf) {
            Ok(0) | Err(_) => break,
            Ok(n) => {
                let data = buf[..n].to_vec();
                let mut s = state.lock().unwrap();
                if s.streaming {
                    drop(s);
                    let _ = match output {
                        Output::Stdout => io::stdout().write_all(&data),
                        Output::Stderr => io::stderr().write_all(&data),
                    };
                } else {
                    s.buffer.push((output, data));
                }
            }
        }
    }
}

fn main() -> ExitCode {
    let args: Vec<String> = env::args().skip(1).collect();

    let (timeout, cmd_start) = match args.first().map(|s| s.as_str()) {
        Some("-t") => {
            let secs: f64 = match args.get(1) {
                None => {
                    eprintln!("quiet: -t requires a numeric argument");
                    return ExitCode::from(1);
                }
                Some(s) => match s.parse() {
                    Ok(v) => v,
                    Err(_) => {
                        eprintln!("quiet: invalid timeout '{}', -t requires a numeric argument", s);
                        return ExitCode::from(1);
                    }
                },
            };
            (Some(secs), 2)
        }
        _ => (None, 0),
    };

    let cmd_args = &args[cmd_start..];
    let cmd_args = if cmd_args.first().map(|s| s.as_str()) == Some("--") {
        &cmd_args[1..]
    } else {
        cmd_args
    };

    if cmd_args.is_empty() {
        eprintln!("Usage: quiet [-t <seconds>] [--] <command> [args...]");
        return ExitCode::from(1);
    }

    let mut child = match Command::new(&cmd_args[0])
        .args(&cmd_args[1..])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
    {
        Ok(c) => c,
        Err(e) => {
            eprintln!("quiet: {}: {}", cmd_args[0], e);
            return ExitCode::from(127);
        }
    };

    let state = Arc::new(Mutex::new(State {
        streaming: false,
        buffer: Vec::new(),
    }));

    let stdout = child.stdout.take().unwrap();
    let stderr = child.stderr.take().unwrap();

    let s1 = Arc::clone(&state);
    let t1 = thread::spawn(move || read_pipe(stdout, Output::Stdout, &s1));

    let s2 = Arc::clone(&state);
    let t2 = thread::spawn(move || read_pipe(stderr, Output::Stderr, &s2));

    let timeout_cancel = if let Some(secs) = timeout {
        let s3 = Arc::clone(&state);
        let (tx, rx) = std::sync::mpsc::channel::<()>();
        thread::spawn(move || {
            use std::sync::mpsc::RecvTimeoutError;
            if rx.recv_timeout(Duration::from_secs_f64(secs)) == Err(RecvTimeoutError::Timeout) {
                let mut s = s3.lock().unwrap();
                if !s.streaming {
                    s.flush();
                }
            }
        });
        Some(tx)
    } else {
        None
    };

    let status = child.wait().expect("failed to wait on child");
    t1.join().unwrap();
    t2.join().unwrap();
    drop(timeout_cancel);

    let mut s = state.lock().unwrap();
    if !s.streaming && !status.success() {
        s.flush();
    }

    #[cfg(unix)]
    let code = {
        use std::os::unix::process::ExitStatusExt;
        status
            .code()
            .unwrap_or_else(|| status.signal().map(|s| 128 + s).unwrap_or(1))
    };
    #[cfg(not(unix))]
    let code = status.code().unwrap_or(1);

    ExitCode::from(code as u8)
}