use clap::Parser; use glob::glob; use regex::Regex; use std::path::{Path, PathBuf}; #[derive(Parser, Debug)] #[command(author, version, about)] struct Args { /// Path to the directory where the files to be renamed are located. #[arg(short = 'i', long)] path: String, /// Regular expression pattern to match against the filenames in the directory. /// /// The pattern should contain match groups (expressed in parentheses). /// /// Example: "S(\d+)E(\d+)" #[arg(short = 's', long)] pattern: String, /// Replacement pattern for the matched filenames. /// /// You can use `\1`, `\2`, etc. to reference the match groups in the pattern. /// /// There are also the following tags available: /// /// * {full_name} - file name with extension /// /// * {name} - filename without extension /// /// * {ext} - extension /// /// Example: "Season \1 Episode \2.{ext}" #[arg(short = 'r', long)] replacement: String, /// Filename wildcard to filter the files to be renamed in the directory. /// /// For example, "*.mkv" will match all files with the ".mkv" extension. #[arg(short = 'w', long, default_value = "*")] wildcard: String, } fn get_file_list(dir_path: &str, wildcard: &str) -> Vec { let pattern = Path::new(dir_path).join(wildcard); let dir_entries: Vec> = glob(&pattern.to_string_lossy()).unwrap().collect(); dir_entries .into_iter() .filter_map(|entry| entry.ok()) .filter(|entry: &PathBuf| entry.is_file()) .map(|entry| entry.file_name().unwrap().to_string_lossy().to_string()) .collect() } fn replace_extra_tags(original_file_name: &str, new_file_name: &str) -> String { let path = Path::new(original_file_name); let name = path.file_stem().unwrap().to_str().unwrap(); let extension = path.extension().unwrap().to_str().unwrap(); let re = Regex::new(r"(\{[^{}]+})").unwrap(); re.find_iter(new_file_name) .map(|m| { let tag = m.as_str(); match tag { "{ext}" => new_file_name.replace(tag, extension), "{full_name}" => new_file_name.replace(tag, original_file_name), "{name}" => new_file_name.replace(tag, name), _ => panic!("Invalid tag: {}", tag), } }) .fold(String::new(), |result, s| result + &s) } fn replace_name(file_name: &str, pattern: &str, replacement: &str) -> String { let mut new_file_name = String::from(file_name); let re = Regex::new(pattern).unwrap(); for captures in re.captures_iter(file_name) { let mut replaced_text = String::from(replacement); for (i, capture) in captures.iter().enumerate() { if let Some(capture) = capture { let capture_text = capture.as_str().to_string(); let replace_key = format!("\\{}", i); replaced_text = replaced_text.replace(&replace_key, &capture_text); } } new_file_name = replaced_text } replace_extra_tags(file_name, &new_file_name) } fn main() { let args = Args::parse(); let dir_path = &args.path; let pattern = &args.pattern; let replacement = &args.replacement; let wildcard = &args.wildcard; let files = get_file_list(dir_path, wildcard); for file_name in files { let replaced_name = replace_name(&file_name, pattern, replacement); println!("mv \"{}\" \"{}\"", file_name, replaced_name); } } #[test] fn groups_and_ext_test() { const INPUT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv"; const RESULT: &str = "Season 1 - Episode 16 - The Best Show - Bla Bla Bla - 720p-HDTV.mkv"; const PATTERN: &str = r"(.*) - S0?(\d+)E0?(\d+) - (.*) (HDTV|WEBDL)-(720p|1080p)"; const REPLACEMENT: &str = r"Season \2 - Episode \3 - \1 - \4 - \6-\5.{ext}"; assert_eq!(replace_name(INPUT, PATTERN, REPLACEMENT), RESULT); } #[test] fn name_test() { const INPUT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv"; const RESULT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.crap"; const PATTERN: &str = r""; const REPLACEMENT: &str = r"{name}.crap"; assert_eq!(replace_name(INPUT, PATTERN, REPLACEMENT), RESULT); } #[test] fn full_name_test() { const INPUT: &str = "The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv"; const RESULT: &str = "REVIEW - The Best Show - S01E16 - Bla Bla Bla HDTV-720p.mkv"; const PATTERN: &str = r""; const REPLACEMENT: &str = r"REVIEW - {full_name}"; assert_eq!(replace_name(INPUT, PATTERN, REPLACEMENT), RESULT); } #[test] fn file_list_test() { const DIRECTORY: &str = "."; const WILDCARD: &str = "*.toml"; assert_eq!(get_file_list(DIRECTORY, WILDCARD), vec!["Cargo.toml"]); } #[test] fn empty_file_list_test() { const DIRECTORY: &str = "src"; const WILDCARD: &str = "*.exe"; assert_eq!(get_file_list(DIRECTORY, WILDCARD), Vec::::new()); }