use clap::{Parser, Subcommand};
use std::path::PathBuf;
const AFTER_LONG_HELP: &str = "Examples:
uncomment src/ Remove comments from every file under src/
uncomment src/ --dry-run --diff Preview changes as a diff, write nothing
uncomment main.rs --remove-doc Also strip doc comments and docstrings
uncomment . -j 0 Process the whole tree using all CPU cores
uncomment init Generate a .uncommentrc.toml for this project
Preserved by default: TODO, FIXME, HACK, XXX, NOSONAR, the ~keep marker, doc
comments, and linting directives (eslint-disable, clippy::, noqa, ...). Override
with the flags above or a .uncommentrc.toml (see `uncomment init`).";
#[derive(Parser, Debug)]
#[command(
name = "uncomment",
version,
about = "Strip comments from source code — accurately, via tree-sitter.",
long_about = "uncomment removes comments from source code using tree-sitter AST parsing, so it \
is 100% accurate and never touches comment-like text inside strings. It preserves \
what matters by default — TODO/FIXME, docs, and linting directives — across 300+ \
languages, with parallel processing and a safe dry-run mode.",
styles = crate::ui::clap_styles(),
after_long_help = AFTER_LONG_HELP
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Commands>,
#[command(flatten)]
pub args: ProcessArgs,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(about = "Create a template configuration file")]
Init {
#[arg(short, long, value_name = "FILE", default_value = ".uncommentrc.toml")]
output: PathBuf,
#[arg(short, long)]
force: bool,
#[arg(long, help = "Generate comprehensive config with all supported languages")]
comprehensive: bool,
#[arg(short, long, help = "Interactive mode to select languages and options")]
interactive: bool,
},
}
#[derive(Parser, Debug)]
pub struct ProcessArgs {
#[arg(value_name = "PATH", help = "Files, directories, or glob patterns to process")]
pub paths: Vec<String>,
#[arg(
short = 'r',
long,
help = "Remove TODO comments (normally preserved)",
help_heading = "Comment selection"
)]
pub remove_todo: bool,
#[arg(
short = 'f',
long,
help = "Remove FIXME comments (normally preserved)",
help_heading = "Comment selection"
)]
pub remove_fixme: bool,
#[arg(
short = 'd',
long,
help = "Remove documentation comments and docstrings",
help_heading = "Comment selection"
)]
pub remove_doc: bool,
#[arg(
short = 'i',
long = "ignore",
value_name = "PATTERN",
help = "Additional patterns to preserve (can be used multiple times)",
help_heading = "Comment selection"
)]
pub ignore_patterns: Vec<String>,
#[arg(
long = "no-default-ignores",
help = "Disable built-in preservation patterns (ESLint, Clippy, etc.)",
help_heading = "Comment selection"
)]
pub no_default_ignores: bool,
#[arg(
short = 'n',
long,
help = "Show changes without modifying files",
help_heading = "Output"
)]
pub dry_run: bool,
#[arg(
long = "diff",
help = "Show line-by-line diffs for modified files (only useful with --dry-run)",
help_heading = "Output"
)]
pub diff: bool,
#[arg(
short = 'v',
long,
help = "Show detailed processing information",
help_heading = "Output"
)]
pub verbose: bool,
#[arg(
long = "no-gitignore",
help = "Process files ignored by .gitignore",
help_heading = "File selection"
)]
pub no_gitignore: bool,
#[arg(
long = "traverse-git-repos",
help = "Traverse into other git repositories (useful for monorepos)",
help_heading = "File selection"
)]
pub traverse_git_repos: bool,
#[arg(
short = 'j',
long = "threads",
value_name = "N",
help = "Number of parallel threads (0 = auto-detect)",
default_value = "1",
help_heading = "Performance"
)]
pub threads: usize,
#[arg(
short = 'c',
long = "config",
value_name = "FILE",
help = "Path to configuration file (overrides automatic discovery)",
help_heading = "File selection"
)]
pub config: Option<PathBuf>,
}
impl ProcessArgs {
pub fn processing_options(&self) -> crate::processor::ProcessingOptions {
crate::processor::ProcessingOptions {
remove_todo: self.remove_todo,
remove_fixme: self.remove_fixme,
remove_doc: self.remove_doc,
custom_preserve_patterns: self.ignore_patterns.clone(),
use_default_ignores: !self.no_default_ignores,
dry_run: self.dry_run,
show_diff: self.diff,
respect_gitignore: !self.no_gitignore,
traverse_git_repos: self.traverse_git_repos,
}
}
}
impl Cli {
pub fn handle_init_command(
output: &PathBuf,
force: bool,
comprehensive: bool,
interactive: bool,
) -> anyhow::Result<()> {
if output.exists() && !force {
return Err(anyhow::anyhow!(
"Configuration file already exists: {}. Use --force to overwrite.",
output.display()
));
}
let (template, detected_info) = if comprehensive {
(crate::config::Config::comprehensive_template_clean(), None)
} else if interactive {
(crate::config::Config::interactive_template_clean()?, None)
} else {
let current_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let (template, info) = crate::config::Config::smart_template_with_info(¤t_dir)?;
(template, Some(info))
};
std::fs::write(output, template)?;
use crate::ui;
anstream::println!(
"{} {} {}",
ui::success(ui::CHECK),
ui::success("Created configuration file:"),
ui::path(output)
);
if comprehensive {
anstream::println!(
"{} Generated comprehensive config with 15+ language configurations",
ui::dim(ui::BULLET)
);
} else if interactive {
anstream::println!(
"{} Generated customized config based on your selections",
ui::dim(ui::BULLET)
);
} else if let Some(info) = detected_info {
if !info.detected_languages.is_empty() {
anstream::println!(
"{} Detected {} file types in your project:",
ui::dim(ui::BULLET),
ui::accent(info.detected_languages.len())
);
for (lang, count) in &info.detected_languages {
anstream::println!(" {}", ui::dim(format!("{count} ({lang} files)")));
}
anstream::println!(
"{} Configured {} languages with appropriate settings",
ui::dim(ui::BULLET),
ui::accent(info.configured_languages)
);
} else {
anstream::println!(
"{} No supported files detected, generated basic template",
ui::dim(ui::BULLET)
);
}
if info.total_files > 0 {
anstream::println!(
"{} Scanned {} files total",
ui::dim(ui::BULLET),
ui::accent(info.total_files)
);
}
} else {
anstream::println!(
"{} Generated smart config based on detected files in your project",
ui::dim(ui::BULLET)
);
}
Ok(())
}
}