use std::io::{self, Write};
use crate::commands::OutputFormat;
#[derive(Debug, Clone, Copy)]
pub struct CliOutput {
pub quiet: bool,
pub format: OutputFormat,
}
impl CliOutput {
pub fn new(quiet: bool, format: OutputFormat) -> Self {
Self { quiet, format }
}
pub fn is_json(self) -> bool {
self.format.is_json()
}
pub fn info(&self, msg: &str) {
if !self.quiet && !self.is_json() {
eprintln!("{msg}");
}
}
pub fn error(&self, msg: &str) -> anyhow::Result<()> {
if self.is_json() {
let json = serde_json::json!({ "error": msg });
write_stdout(&json.to_string())
} else {
eprintln!("Error: {msg}");
Ok(())
}
}
pub fn json(&self, value: &impl serde::Serialize) -> anyhow::Result<()> {
write_stdout(&serde_json::to_string(value)?)
}
}
pub fn reset_sigpipe() {
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
pub fn write_stdout(s: &str) -> anyhow::Result<()> {
let result = (|| -> io::Result<()> {
let stdout = io::stdout();
let mut handle = stdout.lock();
handle.write_all(s.as_bytes())?;
handle.write_all(b"\n")?;
handle.flush()
})();
match result {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
std::process::exit(0);
}
Err(e) => Err(anyhow::anyhow!("Failed to write to stdout: {e}")),
}
}
pub fn is_broken_pipe(err: &anyhow::Error) -> bool {
for cause in err.chain() {
if let Some(io_err) = cause.downcast_ref::<io::Error>() {
if io_err.kind() == io::ErrorKind::BrokenPipe {
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_broken_pipe_detects_broken_pipe() {
let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed");
let anyhow_err = anyhow::Error::new(io_err);
assert!(is_broken_pipe(&anyhow_err));
}
#[test]
fn test_is_broken_pipe_rejects_other_errors() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let anyhow_err = anyhow::Error::new(io_err);
assert!(!is_broken_pipe(&anyhow_err));
}
#[test]
fn test_is_broken_pipe_detects_nested_broken_pipe() {
let io_err = io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed");
let anyhow_err: anyhow::Error = anyhow::anyhow!("write failed").context(io_err.to_string());
assert!(!is_broken_pipe(&anyhow_err));
let io_err2 = io::Error::new(io::ErrorKind::BrokenPipe, "pipe closed");
let anyhow_err2 = anyhow::Error::new(io_err2).context("write failed");
assert!(is_broken_pipe(&anyhow_err2));
}
#[test]
fn test_write_stdout_succeeds_with_valid_output() {
assert!(write_stdout("hello").is_ok());
}
#[test]
fn cli_output_new() {
let out = CliOutput::new(false, OutputFormat::Text);
assert!(!out.quiet);
assert!(!out.is_json());
}
#[test]
fn cli_output_json_mode() {
let out = CliOutput::new(false, OutputFormat::Json);
assert!(out.is_json());
}
#[test]
fn cli_output_quiet_mode() {
let out = CliOutput::new(true, OutputFormat::Text);
assert!(out.quiet);
assert!(!out.is_json());
}
#[test]
fn cli_output_info_does_not_panic_when_quiet() {
let out = CliOutput::new(true, OutputFormat::Text);
out.info("this should be suppressed");
}
#[test]
fn cli_output_info_does_not_panic_when_json() {
let out = CliOutput::new(false, OutputFormat::Json);
out.info("this should be suppressed in json mode");
}
#[test]
fn cli_output_info_does_not_panic_in_normal_mode() {
let out = CliOutput::new(false, OutputFormat::Text);
out.info("normal message");
}
#[test]
fn cli_output_error_text_mode() {
let out = CliOutput::new(false, OutputFormat::Text);
assert!(out.error("test error").is_ok());
}
#[test]
fn cli_output_error_json_mode() {
let out = CliOutput::new(false, OutputFormat::Json);
assert!(out.error("test error").is_ok());
}
#[test]
fn cli_output_json_serializes_value() {
let out = CliOutput::new(false, OutputFormat::Json);
let val = serde_json::json!({"key": "value"});
assert!(out.json(&val).is_ok());
}
#[test]
fn cli_output_debug_impl() {
let out = CliOutput::new(false, OutputFormat::Text);
let debug = format!("{:?}", out);
assert!(debug.contains("CliOutput"));
assert!(debug.contains("quiet: false"));
}
#[test]
fn cli_output_clone() {
let out = CliOutput::new(true, OutputFormat::Json);
let cloned = out;
assert_eq!(out.quiet, cloned.quiet);
assert_eq!(out.is_json(), cloned.is_json());
}
#[test]
fn is_broken_pipe_with_non_io_error() {
let err = anyhow::anyhow!("not an IO error");
assert!(!is_broken_pipe(&err));
}
#[test]
fn is_broken_pipe_with_permission_denied() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "no access");
let err = anyhow::Error::new(io_err);
assert!(!is_broken_pipe(&err));
}
}