110 lines
3.2 KiB
Rust
110 lines
3.2 KiB
Rust
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<String> = 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};
|