use std::io::IsTerminal;
use std::process::ExitCode;
use clap::Parser;
use tracing_subscriber::EnvFilter;
use bzr::cli::Cli;
#[cfg(test)]
use bzr::cli::Commands;
use bzr::error::{self, BzrError};
use bzr::types::OutputFormat;
#[cfg_attr(test, mutants::skip)]
#[tokio::main(flavor = "current_thread")]
async fn main() -> ExitCode {
let cli = Cli::parse();
let filter =
match tracing_filter_directive(cli.quiet, cli.verbose, std::env::var("RUST_LOG").is_ok()) {
Some(directive) => EnvFilter::new(directive),
None => EnvFilter::from_default_env(),
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
if cli.no_color || !std::io::stdout().is_terminal() {
colored::control::set_override(false);
}
let format = match resolve_format(&cli) {
Ok(f) => f,
Err(e) => {
#[expect(clippy::print_stderr)]
{
eprintln!("error: {e}");
}
return exit_code(&e);
}
};
if cli.quiet {
suppress_stdout();
}
let stdout = std::io::stdout();
let stderr = std::io::stderr();
let mut out = stdout.lock();
let mut err = stderr.lock();
let mut writers = bzr::output::writers::Writers::new(&mut out, &mut err);
if let Err(e) = bzr::dispatch(&cli, format, &mut writers).await {
#[expect(clippy::print_stderr)]
{
eprintln!("{}", format_dispatch_error(&e, format));
}
return exit_code(&e);
}
ExitCode::SUCCESS
}
fn exit_code(e: &BzrError) -> ExitCode {
ExitCode::from(u8::try_from(e.exit_code()).unwrap_or(1))
}
fn format_dispatch_error(err: &BzrError, format: OutputFormat) -> String {
if format == OutputFormat::Json {
let json_err = serde_json::json!({
"error": {
"type": err.error_type(),
"message": err.to_string(),
"exit_code": err.exit_code(),
}
});
serde_json::to_string(&json_err)
.unwrap_or_else(|_| r#"{"error":{"message":"serialization failed"}}"#.into())
} else {
format!("error: {err}")
}
}
fn tracing_filter_directive(quiet: bool, verbose: u8, rust_log_set: bool) -> Option<&'static str> {
if quiet {
return Some("off");
}
if rust_log_set {
return None;
}
Some(match verbose {
0 => "bzr=warn",
1 => "bzr=info",
2 => "bzr=debug",
_ => "bzr=trace",
})
}
#[cfg(unix)]
fn suppress_stdout() {
use std::os::unix::io::AsRawFd;
if let Ok(devnull) = std::fs::OpenOptions::new().write(true).open("/dev/null") {
extern "C" {
fn dup2(oldfd: std::ffi::c_int, newfd: std::ffi::c_int) -> std::ffi::c_int;
}
unsafe {
dup2(devnull.as_raw_fd(), 1);
}
}
}
#[cfg_attr(test, mutants::skip)]
#[cfg(windows)]
fn suppress_stdout() {
use std::os::windows::io::IntoRawHandle;
const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5; extern "system" {
fn SetStdHandle(nstdhandle: u32, hhandle: *mut std::ffi::c_void) -> i32;
}
if let Ok(nul) = std::fs::OpenOptions::new().write(true).open("NUL") {
let handle = nul.into_raw_handle();
unsafe {
SetStdHandle(STD_OUTPUT_HANDLE, handle);
}
}
}
#[cfg_attr(test, mutants::skip)]
#[cfg(not(any(unix, windows)))]
fn suppress_stdout() {
}
fn resolve_format(cli: &Cli) -> error::Result<OutputFormat> {
if cli.json {
if cli.output.is_some() {
tracing::warn!("--output ignored because --json takes precedence");
}
return Ok(OutputFormat::Json);
}
if let Some(out) = cli.output {
return Ok(out);
}
if let Ok(val) = std::env::var("BZR_OUTPUT") {
return val.parse().map_err(BzrError::InputValidation);
}
if std::io::stdout().is_terminal() {
Ok(OutputFormat::Table)
} else {
Ok(OutputFormat::Json)
}
}
#[cfg(test)]
#[path = "main_tests.rs"]
mod tests;