use anyhow::Result;
use clap::Parser;
#[derive(Parser, Debug, Clone)]
#[command(
name = "rcmp",
version,
about = "Compare large filesets efficiently (metadata only)",
long_about = "`rcmp` is a tool for comparing large filesets based on metadata attributes.
Currently supports metadata comparison only (no content checking).
By default, differences are printed to stdout. Use --log to write them to a file
instead, or --quiet to suppress stdout output.
EXIT CODES:
0 - No differences found
1 - Differences found
2 - Errors occurred during comparison
EXAMPLES:
# Compare two directories (differences printed to stdout)
rcmp /foo /bar --progress --summary
# Compare and log differences to a file
rcmp /foo /bar --progress --summary --log compare.log
# Compare silently (only exit code)
rcmp /foo /bar --quiet"
)]
struct Args {
#[arg(
long,
default_value = "f:mtime,size d:mtime l:mtime",
value_name = "SETTINGS",
help_heading = "Comparison options"
)]
metadata_compare: String,
#[arg(short = 'e', long = "fail-early", help_heading = "Comparison options")]
fail_early: bool,
#[arg(short = 'm', long = "exit-early", help_heading = "Comparison options")]
exit_early: bool,
#[arg(long, help_heading = "Comparison options")]
expand_missing: bool,
#[arg(long, help_heading = "Comparison options")]
no_check: bool,
#[arg(long, value_name = "PATH", help_heading = "Comparison options")]
log: Option<std::path::PathBuf>,
#[arg(long, value_name = "PATTERN", action = clap::ArgAction::Append, help_heading = "Filtering")]
include: Vec<String>,
#[arg(long, value_name = "PATTERN", action = clap::ArgAction::Append, help_heading = "Filtering")]
exclude: Vec<String>,
#[arg(long, value_name = "PATH", conflicts_with_all = ["include", "exclude"], help_heading = "Filtering")]
filter_file: Option<std::path::PathBuf>,
#[arg(long, help_heading = "Progress & output")]
summary: bool,
#[arg(short = 'q', long = "quiet", help_heading = "Progress & output")]
quiet: bool,
#[arg(
long,
default_value = "json",
value_name = "FORMAT",
help_heading = "Progress & output"
)]
output_format: common::cmp::OutputFormat,
#[arg(long, value_name = "N", help_heading = "Performance & throttling")]
max_open_files: Option<usize>,
#[arg(
long,
default_value = "0",
value_name = "SIZE",
help_heading = "Performance & throttling"
)]
chunk_size: u64,
#[command(flatten)]
common: common::cli::CommonArgs,
#[arg()]
src: std::path::PathBuf,
#[arg()]
dst: std::path::PathBuf,
}
async fn async_main(args: Args) -> Result<common::cmp::FormattedSummary> {
let filter = common::filter::FilterSettings::from_args(
args.filter_file.as_deref(),
&args.include,
&args.exclude,
)?;
let use_stdout = args.log.is_none() && !args.quiet;
let log_handle =
common::cmp::LogWriter::new(args.log.as_deref(), use_stdout, args.output_format).await?;
let summary = common::cmp(
&args.src,
&args.dst,
&log_handle,
&common::cmp::Settings {
fail_early: args.fail_early,
exit_early: args.exit_early,
expand_missing: args.expand_missing,
compare: common::parse_compare_settings(&args.metadata_compare)?,
filter,
},
)
.await?;
log_handle.flush().await?;
Ok(common::cmp::FormattedSummary {
summary,
format: args.output_format,
})
}
fn main() -> Result<()> {
let args = Args::parse();
let func = {
let args = args.clone();
|| async_main(args)
};
let output = common::OutputConfig {
suppress_runtime_stats: matches!(args.output_format, common::cmp::OutputFormat::Json),
..args.common.output_config(args.quiet, args.summary)
};
let runtime = args.common.runtime_config();
let throttle = args
.common
.throttle_config(args.max_open_files, args.chunk_size);
let tracing = common::TracingConfig::local("rcmp");
let progress = if args.common.progress || args.common.progress_type.is_some() {
Some(common::ProgressSettings {
progress_type: common::GeneralProgressType::User {
progress_type: args.common.progress_type.unwrap_or_default(),
kind: common::progress::LocalProgressKind::Compare,
},
progress_delay: args.common.progress_delay.clone(),
})
} else {
None
};
let res = common::run(progress, output, runtime, throttle, tracing, func);
match res {
Some(formatted) => {
if args.no_check {
std::process::exit(0)
} else {
for (_, cmp_result) in &formatted.summary.mismatch {
let different = cmp_result[common::cmp::CompareResult::Different] > 0
|| cmp_result[common::cmp::CompareResult::SrcMissing] > 0
|| cmp_result[common::cmp::CompareResult::DstMissing] > 0;
if different {
std::process::exit(1);
}
}
std::process::exit(0);
}
}
None => std::process::exit(2),
}
}