use anyhow::{Context, Result};
use clap::Parser;
use std::io::{self, Read, Write, BufRead, IsTerminal};
use std::fs;
use std::env;
use log::{info, LevelFilter};
use dotenvy;
use cleansh::commands;
use cleansh::logger;
use cleansh::ui;
use cleansh::utils::app_state::AppState;
use cleansh::utils::platform;
use cleansh::cli::{Cli, Commands};
fn main() -> Result<()> {
dotenvy::dotenv().ok();
let cli = Cli::parse();
let effective_log_level = if cli.quiet {
Some(LevelFilter::Warn)
} else if cli.debug && !cli.disable_debug {
Some(LevelFilter::Debug)
} else if cli.disable_debug {
Some(LevelFilter::Info) } else {
None };
logger::init_logger(effective_log_level);
info!("cleansh started. Version: {}", env!("CARGO_PKG_VERSION"));
let app_state_path = if let Some(dir) = dirs::data_dir() {
dir.join("cleansh").join("state.json")
} else {
std::env::current_dir()?.join("cleansh_state.json")
};
let app_state = AppState::load(&app_state_path)?;
let theme_map = ui::theme::build_theme_map(cli.theme.as_ref())?;
let stderr_supports_color = io::stderr().is_terminal();
match cli.command {
Some(Commands::Stats(stats_opts)) => {
let input_content = if let Some(path) = cli.input_file.as_ref() {
commands::cleansh::info_msg(format!("Reading input from file: {}", path.display()), &theme_map);
fs::read_to_string(path)
.with_context(|| format!("Failed to read input from {}", path.display()))?
} else if io::stdin().is_terminal() {
commands::cleansh::info_msg(
&format!("Reading input from stdin. Press {} then Enter to finish input.", platform::eof_key_combo()),
&theme_map,
);
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)
.context("Failed to read from stdin")?;
buffer
} else {
commands::cleansh::info_msg("Reading input from stdin for stats analysis...", &theme_map);
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)
.context("Failed to read from stdin")?;
buffer
};
commands::stats::run_stats_command(
&input_content,
cli.config.clone(),
cli.rules.clone(),
&theme_map,
cli.enable.clone(),
cli.disable.clone(),
stats_opts.json_file.clone(),
stats_opts.json_stdout,
stats_opts.sample_matches,
stats_opts.fail_over_threshold,
cli.disable_donation_prompts,
)?;
}
Some(Commands::Uninstall { yes }) => {
commands::uninstall::run_uninstall_command(yes, &theme_map)?;
}
None => {
let effective_diff = cli.diff && !cli.no_diff;
let effective_clipboard = cli.clipboard && !cli.no_clipboard;
if cli.line_buffered {
if effective_diff {
commands::cleansh::error_msg("Error: --line-buffered is incompatible with --diff.", &theme_map);
std::process::exit(1);
}
if effective_clipboard {
commands::cleansh::error_msg("Error: --line-buffered is incompatible with --clipboard.", &theme_map);
std::process::exit(1);
}
if cli.input_file.is_some() {
commands::cleansh::error_msg("Error: --line-buffered is incompatible with --input-file. Use piping for streaming input.", &theme_map);
std::process::exit(1);
}
}
if cli.line_buffered && cli.input_file.is_none() && !io::stdin().is_terminal() {
let mut output_writer: Box<dyn Write> = if let Some(path) = &cli.output {
commands::cleansh::warn_msg(
"Warning: --line-buffered is intended for real-time console output. \
Outputting to a file (--output) will still buffer by line, \
but real-time benefits might be less apparent.",
&theme_map,
);
Box::new(std::fs::File::create(path)?)
} else {
if !cli.quiet {
commands::cleansh::info_msg(
"Reading input from stdin in real-time, line-buffered mode...",
&theme_map,
);
}
Box::new(io::stdout())
};
let compiled_rules = commands::cleansh::load_and_compile(
cli.config.clone(),
cli.rules.clone(),
cli.enable.clone(),
cli.disable.clone(),
)?;
log::debug!("Compiled rules count: {}", compiled_rules.rules.len());
if compiled_rules.rules.is_empty() {
log::warn!("No rules were compiled. Redactions will not occur.");
}
let mut all_redaction_matches = Vec::new();
let stdin = io::stdin().lock();
for raw_line in stdin.lines() {
let line_content = raw_line?;
log::debug!("Processing line: {:?}", line_content);
let (sanitized_segment, mut line_matches) =
commands::cleansh::sanitize_single_line(&line_content, &compiled_rules);
log::debug!("Sanitized segment: {:?}", sanitized_segment);
log::debug!("Matches found for line: {}", line_matches.len());
output_writer.write_all(sanitized_segment.as_bytes())?;
output_writer.write_all(b"\n")?;
output_writer.flush()?;
all_redaction_matches.append(&mut line_matches);
}
if all_redaction_matches.is_empty() {
if !cli.no_summary {
commands::cleansh::info_msg(
"No redactions applied.",
&theme_map,
);
}
} else {
if !cli.no_summary && !cli.quiet {
let summary = commands::cleansh::build_redaction_summary_from_matches(&all_redaction_matches);
commands::cleansh::info_msg(
"Redaction Summary",
&theme_map,
);
ui::redaction_summary::print_summary(&summary, &mut io::stderr(), &theme_map, stderr_supports_color)?;
}
}
} else {
let input_content = if let Some(path) = cli.input_file.as_ref() {
commands::cleansh::info_msg(format!("Reading input from file: {}", path.display()), &theme_map);
fs::read_to_string(path)
.with_context(|| format!("Failed to read input from {}", path.display()))?
} else if io::stdin().is_terminal() {
commands::cleansh::info_msg(
&format!("Reading input from stdin. Press {} then Enter to finish input.", platform::eof_key_combo()),
&theme_map,
);
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)
.context("Failed to read from stdin")?;
buffer
} else {
commands::cleansh::info_msg("Reading input from stdin...", &theme_map);
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)
.context("Failed to read from stdin")?;
buffer
};
let opts = commands::cleansh::CleanshOptions {
input: input_content,
clipboard: effective_clipboard,
diff: effective_diff,
config_path: cli.config.clone(),
rules_config_name: cli.rules.clone(),
output_path: cli.output.clone(),
no_redaction_summary: cli.no_summary,
enable_rules: cli.enable.clone(),
disable_rules: cli.disable.clone(),
};
if let Err(e) = commands::cleansh::run_cleansh_opts(opts, &theme_map) {
commands::cleansh::error_msg(
&format!("An error occurred: {}", e),
&theme_map,
);
std::process::exit(1);
}
}
}
}
app_state.save(&app_state_path)?;
info!("cleansh finished successfully.");
Ok(())
}