feat(cps): initial commit

This commit is contained in:
2025-12-12 11:58:49 -05:00
commit 66a927eae8
4 changed files with 442 additions and 0 deletions

118
src/main.rs Normal file
View File

@@ -0,0 +1,118 @@
use anyhow::{Context, Result};
use clap::Parser;
use globset::{Glob, GlobSet, GlobSetBuilder};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
#[derive(Parser, Debug)]
#[command(name = "cps")]
#[command(about = "Recursively copy directory structures and specific files based on patterns.")]
#[command(version, long_about = None)]
struct Cli {
#[arg(value_name = "SOURCE")]
source: PathBuf,
#[arg(value_name = "DEST")]
dest: PathBuf,
#[arg(short, long)]
recursive: bool,
#[arg(short = 'e', long = "extension", value_name = "PATTERN")]
patterns: Vec<String>,
#[arg(short = 'd', long = "no-empty-dir", visible_alias = "prune")]
prune: bool,
#[arg(long)]
dry_run: bool,
#[arg(short, long)]
verbose: bool,
}
fn main() -> Result<()> {
let cli = Cli::parse();
if !cli.source.exists() {
anyhow::bail!("Source path does not exist: {:?}", cli.source);
}
let mut builder = GlobSetBuilder::new();
if !cli.patterns.is_empty() {
for pattern in &cli.patterns {
builder.add(Glob::new(pattern).context("Failed to compile glob pattern")?);
}
}
let glob_set = builder.build().context("Failed to build glob set")?;
let has_patterns = !cli.patterns.is_empty();
let max_depth = if cli.recursive { usize::MAX } else { 1 };
let walker = WalkDir::new(&cli.source)
.max_depth(max_depth)
.follow_links(false)
.into_iter();
for entry in walker.filter_map(|e| e.ok()) {
let path = entry.path();
if path == cli.source {
continue;
}
let relative_path = path.strip_prefix(&cli.source)?;
let target_path = cli.dest.join(relative_path);
if path.is_dir() {
if !cli.prune {
create_dir(&target_path, cli.dry_run, cli.verbose)?;
}
} else if path.is_file() {
if has_patterns {
let filename = match path.file_name() {
Some(n) => n,
None => continue,
};
if glob_set.is_match(filename) {
if cli.prune {
if let Some(parent) = target_path.parent() {
create_dir(parent, cli.dry_run, cli.verbose)?;
}
}
copy_file(path, &target_path, cli.dry_run, cli.verbose)?;
}
}
}
}
Ok(())
}
fn create_dir(path: &Path, dry_run: bool, verbose: bool) -> Result<()> {
if !path.exists() {
if verbose {
println!("mkdir: {:?}", path);
}
if !dry_run {
fs::create_dir_all(path).context(format!("Failed to create dir {:?}", path))?;
}
}
Ok(())
}
fn copy_file(src: &Path, dest: &Path, dry_run: bool, verbose: bool) -> Result<()> {
if verbose {
println!("copy: {:?} -> {:?}", src, dest);
}
if !dry_run {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(src, dest).context(format!("Failed to copy file {:?}", src))?;
}
Ok(())
}