use std::fmt::Arguments; use std::io::{self, Write}; use std::sync::OnceLock; use std::sync::atomic::{AtomicU8, Ordering}; use anyhow::{Result, anyhow}; // progress, on stderr and only when it was asked for macro_rules! note { ($ctx:expr, $($arg:tt)*) => { if !$ctx.quiet { $crate::output::write_err(format_args!($($arg)*)) } }; } // not progress: it speaks about the result, so --quiet keeps it macro_rules! warning { ($($arg:tt)*) => { $crate::output::write_err(format_args!("warning: {}", format_args!($($arg)*))) }; } // what a command was asked for, on stdout macro_rules! line { ($($arg:tt)*) => { $crate::output::write_line(format_args!($($arg)*)) }; } // the same, for a caller that terminates its own entries macro_rules! text { ($($arg:tt)*) => { $crate::output::write_text(format_args!($($arg)*)) }; } // stdout is open, or it is not and why const OPEN: u8 = 0; // the reader left: nothing is wrong, there is just nowhere to write const CLOSED: u8 = 1; // the write itself failed, so what was asked for was never delivered const FAILED: u8 = 2; static STDOUT: AtomicU8 = AtomicU8::new(OPEN); static REASON: OnceLock = OnceLock::new(); pub(crate) fn write_line(args: Arguments) { if writable() { finish(writeln!(io::stdout(), "{args}")); } } pub(crate) fn write_text(args: Arguments) { if writable() { finish(write!(io::stdout(), "{args}")); } } // a path on unix is bytes, not text, so a record naming one is written as bytes pub(crate) fn write_bytes(bytes: &[u8]) { if writable() { finish(io::stdout().write_all(bytes)); } } // stderr is commentary about the work, not the work itself. there is nowhere to // report that it could not be written, so the error goes nowhere -- eprintln! // panics instead, which turns a closed pipe into a crash pub(crate) fn write_err(args: Arguments) { let _ = writeln!(io::stderr(), "{args}"); } fn writable() -> bool { STDOUT.load(Ordering::Relaxed) == OPEN } // a failed write must not end the process: a command halfway through moving // files would leave the rest undone and still report the success it had planned // on. writing stops, the command runs to its end, and main asks how it went fn finish(written: io::Result<()>) { let Err(e) = written else { return; }; match e.kind() { io::ErrorKind::BrokenPipe => STDOUT.store(CLOSED, Ordering::Relaxed), _ => { STDOUT.store(FAILED, Ordering::Relaxed); let _ = REASON.set(e.to_string()); } } } // whether everything a command was asked for actually reached stdout. the // buffered writes text! makes are flushed here rather than by the runtime after // main returns, which discards the error and truncates the output in silence pub(crate) fn delivered() -> Result<()> { if writable() { finish(io::stdout().flush()); } if STDOUT.load(Ordering::Relaxed) != FAILED { return Ok(()); } Err(match REASON.get() { Some(reason) => anyhow!("writing to stdout failed: {reason}"), None => anyhow!("writing to stdout failed"), }) } pub(crate) use {line, note, text, warning};