comment-stripper-rs 0.1.0

Production-grade CLI to strip comments from Rust source trees using ra-ap-rustc_lexer
Documentation
use anyhow::Result;
use clap::{ArgAction, Parser};
use std::path::PathBuf;

#[derive(Parser, Debug)]
#[command(name = "comment-stripper-rs")]
#[command(about = "Strip non-rustdoc comments from Rust source trees")]
#[command(version)]
struct Cli {
    #[arg(
        default_value = ".",
        value_name = "PATH",
        help = "One or more files or directories to process (defaults to the current directory)"
    )]
    roots: Vec<PathBuf>,

    #[arg(short = 'c', long, action = ArgAction::SetTrue, help = "Report files that would change without rewriting them; exit non-zero if any would")]
    check: bool,

    #[arg(short = 'v', long, action = ArgAction::SetTrue, help = "Print changed file paths")]
    verbose: bool,

    #[arg(long, action = ArgAction::SetTrue, help = "Include hidden files and directories")]
    hidden: bool,

    #[arg(long, action = ArgAction::SetTrue, help = "Follow symlinks")]
    follow_links: bool,

    #[arg(short = 'n', long, action = ArgAction::SetTrue, help = "Do not create backup files when rewriting")]
    no_backup: bool,

    #[arg(short = 'd', long, action = ArgAction::SetTrue, help = "Also strip rustdoc comments (///, //!, /** */, /*! */)")]
    strip_docs: bool,

    #[arg(
        long,
        action = ArgAction::SetTrue,
        conflicts_with = "check",
        help = "Delete backup files (matching --backup-suffix) instead of stripping comments"
    )]
    clean_backups: bool,

    #[arg(
        long,
        action = ArgAction::SetTrue,
        conflicts_with_all = ["check", "clean_backups"],
        help = "Restore backup files (matching --backup-suffix) over their originals instead of stripping comments"
    )]
    restore_backups: bool,

    #[arg(
        long,
        default_value = ".bak",
        help = "Suffix appended to backup files (also matched by --clean-backups)"
    )]
    backup_suffix: String,

    #[arg(
        short = 'e',
        long,
        value_delimiter = ',',
        help = "Comma-separated directory names to skip (target and .git are always excluded)"
    )]
    exclude_dirs: Vec<String>,

    #[arg(
        short = 'i',
        long,
        value_delimiter = ',',
        help = "Comma-separated glob patterns; only matching files are stripped (e.g. 'src/**/*.rs'). Use '**' to cross directories. Excluded dirs still win."
    )]
    include: Vec<String>,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    let mut exclude_dirs = cli.exclude_dirs;
    for forced in ["target", ".git"] {
        if !exclude_dirs.iter().any(|d| d == forced) {
            exclude_dirs.push(forced.to_string());
        }
    }

    let cfg = comment_stripper_rs::Config {
        roots: cli.roots,
        check: cli.check,
        verbose: cli.verbose,
        hidden: cli.hidden,
        follow_links: cli.follow_links,
        no_backup: cli.no_backup,
        strip_doc_comments: cli.strip_docs,
        backup_suffix: cli.backup_suffix,
        exclude_dirs,
        include_globs: cli.include,
    };

    if cli.clean_backups {
        let stats = comment_stripper_rs::remove_backups(&cfg)?;
        if cli.verbose {
            eprintln!("removed {} backup file(s)", stats.files_changed);
        }
        return Ok(());
    }

    if cli.restore_backups {
        let stats = comment_stripper_rs::restore_backups(&cfg)?;
        if cli.verbose {
            eprintln!("restored {} backup file(s)", stats.files_changed);
        }
        return Ok(());
    }

    let stats = comment_stripper_rs::run(&cfg)?;
    if cli.verbose && !cli.check {
        eprintln!(
            "scanned {} Rust file(s), changed {}",
            stats.files_seen, stats.files_changed
        );
    }
    Ok(())
}