use std::io::{self, Read};
use std::path::PathBuf;
use std::fs;
use std::process::exit;
use std::collections::HashMap;
use std::env;
use anyhow::{Context, Result};
use clap::{Parser, ArgAction};
use log::{debug, info};
use dotenvy;
mod commands;
mod config;
mod logger;
mod tools;
mod ui;
#[derive(Parser, Debug)]
#[command(
author = "Cleansh Technologies LLC",
version = env!("CARGO_PKG_VERSION"),
about = "Sanitize your terminal output. One tool. One purpose.",
long_about = "cleansh is a robust and secure command-line utility designed to redact sensitive information from your terminal output before sharing. It supports masking emails, IP addresses, various tokens (JWTs, AWS, GCP), SSH keys, hex secrets, and normalizing absolute paths. Secure by default, zero config required, and extendable when needed."
)]
struct Cli {
#[arg(short, long, help = "Copy the sanitized result to the system clipboard.", env = "CLIPBOARD_ENABLED", action = ArgAction::SetTrue)]
clipboard: bool,
#[arg(long = "no-clipboard", help = "Do NOT copy the sanitized result to the system clipboard.", action = ArgAction::SetTrue)] disable_clipboard: bool,
#[arg(short, long, help = "Show a detailed diff view highlighting redactions.", action = ArgAction::SetTrue)]
diff: bool,
#[arg(long = "no-diff", help = "Do NOT show a detailed diff view.", action = ArgAction::SetTrue)] disable_diff: bool,
#[arg(long, value_name = "FILE", help = "Load a custom YAML configuration file for redaction rules.")]
config: Option<PathBuf>,
#[arg(short = 'o', long, value_name = "FILE", help = "Output the sanitized content to a specified file instead of stdout.")]
out: Option<PathBuf>,
#[arg(long, help = "Enable debug logging for more verbose output.", action = ArgAction::SetTrue)]
debug: bool,
#[arg(long = "no-debug", help = "Do NOT enable debug logging.", action = ArgAction::SetTrue)] disable_debug: bool,
#[arg(value_name = "INPUT", help = "Optional input file to read from. Reads from stdin if not provided.")]
input_file: Option<PathBuf>,
#[arg(long, value_name = "FILE", help = "Load a custom YAML theme file for output styling.")]
theme: Option<PathBuf>,
#[arg(long, help = "Do not display the redaction summary at the end of the output.", action = ArgAction::SetTrue)]
no_redaction_summary: bool,
}
fn main() -> Result<()> {
dotenvy::dotenv().ok();
let cli = Cli::parse();
let effective_debug = cli.debug && !cli.disable_debug;
let effective_clipboard = cli.clipboard && !cli.disable_clipboard;
let effective_diff = cli.diff && !cli.disable_diff;
if effective_debug {
unsafe {
env::set_var("RUST_LOG", "debug");
}
} else if env::var("RUST_LOG").is_err() {
if let Ok(log_level_env) = env::var("LOG_LEVEL") {
unsafe {
env::set_var("RUST_LOG", log_level_env);
}
}
}
logger::init_logger();
info!("cleansh started. Version: {}", env!("CARGO_PKG_VERSION"));
debug!("Parsed CLI arguments: {:?}", cli);
debug!("Effective Debug: {}, Effective Clipboard: {}, Effective Diff: {}", effective_debug, effective_clipboard, effective_diff);
let theme_map: HashMap<ui::theme::ThemeEntry, ui::theme::ThemeStyle> =
if let Some(theme_path) = cli.theme {
ui::theme::ThemeStyle::load_from_file(&theme_path)
.unwrap_or_else(|e| {
ui::output_format::print_warn_message(
&mut io::stderr(),
&format!("Failed to load custom theme from {}: {}. Falling back to default white theme.", theme_path.display(), e),
&ui::theme::ThemeStyle::default_theme_map(),
);
log::warn!("Failed to load custom theme from {}: {}. Falling back to default white theme.", theme_path.display(), e);
ui::theme::ThemeStyle::default_theme_map()
})
} else {
ui::theme::ThemeStyle::default_theme_map()
};
let mut input_content = String::new();
if let Some(input_path) = cli.input_file {
info!("Reading input from file: {}", input_path.display());
ui::output_format::print_info_message(
&mut io::stdout(),
&format!("Reading input from file: {}", input_path.display()),
&theme_map,
);
input_content = fs::read_to_string(&input_path)
.with_context(|| format!("Failed to read input from file: {}", input_path.display()))?;
} else {
info!("Reading input from stdin...");
ui::output_format::print_info_message(
&mut io::stdout(),
"Reading input from stdin...",
&theme_map,
);
io::stdin()
.read_to_string(&mut input_content)
.context("Failed to read input from stdin")?;
}
if let Err(e) = commands::cleansh::run_cleansh(
&input_content,
effective_clipboard, effective_diff, cli.config,
cli.out,
cli.no_redaction_summary,
&theme_map,
) {
ui::output_format::print_error_message(
&mut io::stderr(),
&format!("An error occurred: {}", e),
&theme_map,
);
exit(1);
}
info!("cleansh finished successfully.");
Ok(())
}