From 9cac1b6b503c416c6f17addd895ba9ed8052f484 Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Thu, 12 Sep 2024 15:07:47 +0200 Subject: [PATCH 1/7] Split modules --- src/extensions.rs | 81 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 92 ++++------------------------------------------- 2 files changed, 88 insertions(+), 85 deletions(-) create mode 100644 src/extensions.rs diff --git a/src/extensions.rs b/src/extensions.rs new file mode 100644 index 0000000..64e6ad5 --- /dev/null +++ b/src/extensions.rs @@ -0,0 +1,81 @@ +use std::io::ErrorKind; +use std::path::Path; +use std::process::{self, exit}; + +fn infer_mimetype(path: &Path, mime_type: bool) -> Option { + // TODO: Do something on windows :see_no_evil: + let mut cmd = process::Command::new("file"); + let cmd_with_args = cmd.arg(path).arg("--brief"); + let cmd_with_args = if mime_type { + cmd_with_args.arg("--mime-type") + } else { + cmd_with_args + }; + + let output = cmd_with_args.output(); + match output { + Ok(output) => { + let output_str = String::from_utf8(output.stdout).unwrap(); + let mime_type = match output_str.strip_suffix('\n') { + Some(s) => String::from(s), + None => output_str, + }; + Some(mime_type) + } + Err(e) => match e.kind() { + ErrorKind::NotFound => { + eprintln!("Error: `file` probably not installed"); + exit(-1); + } + _ => panic!("{e}"), + }, + } +} + +pub fn find_extensions_from_content(path: &Path) -> Vec { + let mime_type_based = match infer_mimetype(path, true) { + None => vec![], + Some(mime_type) => { + let mime_type_str = mime_type.as_str(); + match mime_type_str { + "application/pdf" => vec![String::from("pdf")], + "image/jpeg" => vec![String::from("jpeg"), String::from("jpg")], + "image/png" => vec![String::from("png")], + "text/csv" => vec![String::from("csv")], + "text/html" => vec![String::from("html"), String::from("htm")], + "text/x-script.python" => vec![String::from("py"), String::from("pyw")], + _other => vec![], + } + } + }; + + let mut description_based = match infer_mimetype(path, false) { + None => vec![], + Some(description) => { + let description_str = description.as_str(); + match description_str { + "Apache Parquet" => vec![String::from("parquet"), String::from("pq")], + _other => vec![], + } + } + }; + + let mut extensions = mime_type_based.clone(); + extensions.append(&mut description_based); + extensions +} + +pub fn has_correct_extension(path: &Path, possible_extensions: &[String]) -> bool { + if possible_extensions.is_empty() { + true + } else { + let current_extension = path.extension(); + if current_extension.is_none() { + false + } else { + let extension = current_extension.unwrap().to_ascii_lowercase(); + let extension_str = String::from(extension.to_string_lossy()); + possible_extensions.contains(&extension_str) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 806c905..96fe16d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,12 @@ +mod extensions; + use colored::Colorize; use regex::Regex; use std::fmt::{Display, Formatter, Result}; use std::fs::rename; -use std::io::ErrorKind; use std::path::{Path, PathBuf}; -use std::process::{self, exit}; + +use crate::extensions::{find_extensions_from_content, has_correct_extension}; extern crate unidecode; use unidecode::unidecode; @@ -130,97 +132,17 @@ fn suggest_rename(path: &PathBuf, command: &RenameCommand) -> RenameIntent { }; PathBuf::from(new_name) } - }} + }, + } } fn suggest_renames(files: &[PathBuf], command: &RenameCommand) -> Vec { files .iter() - .map(|path| suggest_rename( - path, - command, - )) + .map(|path| suggest_rename(path, command)) .collect() } -fn infer_mimetype(path: &Path, mime_type: bool) -> Option { - // TODO: Do something on windows :see_no_evil: - let mut cmd = process::Command::new("file"); - let cmd_with_args = cmd.arg(path).arg("--brief"); - let cmd_with_args = if mime_type { - cmd_with_args.arg("--mime-type") - } else { - cmd_with_args - }; - - let output = cmd_with_args.output(); - match output { - Ok(output) => { - let output_str = String::from_utf8(output.stdout).unwrap(); - let mime_type = match output_str.strip_suffix('\n') { - Some(s) => String::from(s), - None => output_str, - }; - Some(mime_type) - } - Err(e) => match e.kind() { - ErrorKind::NotFound => { - eprintln!("Error: `file` probably not installed"); - exit(-1); - } - _ => panic!("{e}"), - }, - } -} - -fn find_extensions_from_content(path: &Path) -> Vec { - let mime_type_based = match infer_mimetype(path, true) { - None => vec![], - Some(mime_type) => { - let mime_type_str = mime_type.as_str(); - match mime_type_str { - "application/pdf" => vec![String::from("pdf")], - "image/jpeg" => vec![String::from("jpeg"), String::from("jpg")], - "image/png" => vec![String::from("png")], - "text/csv" => vec![String::from("csv")], - "text/html" => vec![String::from("html"), String::from("htm")], - "text/x-script.python" => vec![String::from("py"), String::from("pyw")], - _other => vec![], - } - } - }; - - let mut description_based = match infer_mimetype(path, false) { - None => vec![], - Some(description) => { - let description_str = description.as_str(); - match description_str { - "Apache Parquet" => vec![String::from("parquet"), String::from("pq")], - _other => vec![], - } - } - }; - - let mut extensions = mime_type_based.clone(); - extensions.append(&mut description_based); - extensions -} - -fn has_correct_extension(path: &Path, possible_extensions: &[String]) -> bool { - if possible_extensions.is_empty() { - true - } else { - let current_extension = path.extension(); - if current_extension.is_none() { - false - } else { - let extension = current_extension.unwrap().to_ascii_lowercase(); - let extension_str = String::from(extension.to_string_lossy()); - possible_extensions.contains(&extension_str) - } - } -} - fn try_rename(path: &Path, new_name: &Path) -> bool { match rename(path, new_name) { Ok(_) => { From 81761cdcc7499df67b94533ecdd615405889fc1c Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Fri, 27 Sep 2024 17:22:28 +0200 Subject: [PATCH 2/7] Reimplement commands --- src/commands.rs | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 124 +++------------------------------------- src/main.rs | 54 ++++++++++-------- 3 files changed, 184 insertions(+), 141 deletions(-) create mode 100644 src/commands.rs diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..07bad28 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,147 @@ +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; +use colored::Colorize; +use regex::Regex; +use unidecode::unidecode; +use crate::extensions::{find_extensions_from_content, has_correct_extension}; + +pub struct RenameIntent { + pub old_name: PathBuf, + pub new_name: PathBuf, +} + +impl RenameIntent { + /// Is the new name different from the old one? + pub fn is_changed(&self) -> bool { + self.old_name != self.new_name + } +} + +impl Display for RenameIntent { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + if self.is_changed() { + write!( + f, + "{0} → {1}", + self.old_name.to_string_lossy().red(), + self.new_name.to_string_lossy().green() + ) + } else { + write!(f, "{0} =", self.old_name.to_string_lossy(),) + } + } +} + + +pub trait RenameCommand { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf; + + fn suggest_renames(&self, files: &[PathBuf]) -> Vec { + files + .iter() + .map(|path| RenameIntent { old_name: path.clone(), new_name: self.suggest_new_name(path) }) + .collect() + } +} + +pub struct Normalize; + +impl RenameCommand for Normalize { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let path_str = old_name.to_string_lossy().to_string(); + let new_name = unidecode(&path_str).replace(' ', "_"); //#.to_lowercase(); + PathBuf::from(new_name) + } +} + +pub struct SetExtension { + pub extension: String, +} + +impl RenameCommand for SetExtension { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let mut new_name = old_name.clone(); + new_name.set_extension(&self.extension); + new_name + } +} + +pub struct Remove { + pub pattern: String, +} + +impl RenameCommand for Remove { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let new_name = old_name.to_string_lossy().replace(&self.pattern, ""); + PathBuf::from(new_name) + } +} + +pub struct Replace { + pub pattern: String, + pub replacement: String, + pub is_regex: bool, +} + +impl RenameCommand for Replace { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let path_str = old_name.to_string_lossy().to_string(); + let new_name = if self.is_regex { + let re = Regex::new(&self.pattern).unwrap(); + re.replace_all(&path_str, &self.replacement).to_string() + } else { + path_str.replace(&self.pattern, &self.replacement) + }; + PathBuf::from(new_name) + } +} + +pub struct ChangeCase { + pub upper: bool, +} + +impl RenameCommand for ChangeCase { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let path_str = old_name.to_string_lossy().to_string(); + let new_name = match self.upper { + true => path_str.to_uppercase(), + false => path_str.to_lowercase(), + }; + PathBuf::from(new_name) + } +} + +pub struct FixExtension { + pub append: bool, +} + +impl RenameCommand for FixExtension { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let possible_extensions = find_extensions_from_content(old_name); + let mut new_name = old_name.clone(); + if !has_correct_extension(old_name, &possible_extensions) { + let mut new_extension = possible_extensions[0].clone(); + if self.append { + let old_extension = new_name.extension(); + if old_extension.is_some() { + new_extension.insert(0, '.'); + new_extension.insert_str(0, old_extension.unwrap().to_str().unwrap()) + } + } + new_name.set_extension(new_extension); + }; + new_name + } +} + +pub struct Prefix { + pub prefix: String, +} + +impl RenameCommand for Prefix { + fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + let mut new_name = self.prefix.clone(); + new_name.push_str(old_name.to_string_lossy().to_string().as_str()); + PathBuf::from(new_name) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 96fe16d..a3897c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,55 +1,16 @@ -mod extensions; +pub mod extensions; +pub mod commands; use colored::Colorize; -use regex::Regex; -use std::fmt::{Display, Formatter, Result}; use std::fs::rename; +use std::ops::Deref; use std::path::{Path, PathBuf}; -use crate::extensions::{find_extensions_from_content, has_correct_extension}; - extern crate unidecode; -use unidecode::unidecode; - -pub struct RenameIntent { - old_name: PathBuf, - new_name: PathBuf, -} - -impl RenameIntent { - /// Is the new name different from the old one? - fn is_changed(&self) -> bool { - self.old_name != self.new_name - } -} - -impl Display for RenameIntent { - fn fmt(&self, f: &mut Formatter) -> Result { - if self.is_changed() { - write!( - f, - "{0} → {1}", - self.old_name.to_string_lossy().red(), - self.new_name.to_string_lossy().green() - ) - } else { - write!(f, "{0} =", self.old_name.to_string_lossy(),) - } - } -} - -pub enum RenameCommand { - SetExtension(String), - Remove(String), - Prefix(String), - FixExtension(bool), - Normalize, - Replace(String, String, bool), - ChangeCase(bool), -} +use crate::commands::{RenameCommand, RenameIntent}; pub struct Config { - pub command: RenameCommand, + pub command: Box, pub dry: bool, pub files: Vec, pub auto_confirm: bool, @@ -74,75 +35,6 @@ fn print_intents(intents: &Vec, show_unchanged: bool) { } } -/// Find a new name for a single file -fn suggest_rename(path: &PathBuf, command: &RenameCommand) -> RenameIntent { - RenameIntent { - old_name: path.clone(), - new_name: match &command { - RenameCommand::SetExtension(extension) => { - let mut new_name = path.clone(); - new_name.set_extension(extension); - new_name - } - RenameCommand::Remove(pattern) => { - let new_name = path.to_string_lossy().replace(pattern, ""); - PathBuf::from(new_name) - } - RenameCommand::Prefix(prefix) => { - let mut new_name = prefix.clone(); - new_name.push_str(path.to_string_lossy().to_string().as_str()); - PathBuf::from(new_name) - } - RenameCommand::Normalize => { - let path_str = path.to_string_lossy().to_string(); - let new_name = unidecode(&path_str).replace(' ', "_"); //#.to_lowercase(); - PathBuf::from(new_name) - } - RenameCommand::FixExtension(append) => { - let possible_extensions = find_extensions_from_content(path); - let mut new_name = path.clone(); - if !has_correct_extension(path, &possible_extensions) { - let mut new_extension = possible_extensions[0].clone(); - if *append { - let old_extension = new_name.extension(); - if old_extension.is_some() { - new_extension.insert(0, '.'); - new_extension.insert_str(0, old_extension.unwrap().to_str().unwrap()) - } - } - new_name.set_extension(new_extension); - }; - new_name - } - RenameCommand::Replace(pattern, replacement, is_regex) => { - let path_str = path.to_string_lossy().to_string(); - let new_name = if *is_regex { - let re = Regex::new(pattern).unwrap(); - re.replace_all(&path_str, replacement).to_string() - } else { - path_str.replace(pattern, replacement) - }; - PathBuf::from(new_name) - } - RenameCommand::ChangeCase(upper) => { - let path_str = path.to_string_lossy().to_string(); - let new_name = match upper { - true => path_str.to_uppercase(), - false => path_str.to_lowercase(), - }; - PathBuf::from(new_name) - } - }, - } -} - -fn suggest_renames(files: &[PathBuf], command: &RenameCommand) -> Vec { - files - .iter() - .map(|path| suggest_rename(path, command)) - .collect() -} - fn try_rename(path: &Path, new_name: &Path) -> bool { match rename(path, new_name) { Ok(_) => { @@ -167,13 +59,13 @@ fn try_rename(path: &Path, new_name: &Path) -> bool { } fn process_command( - command: &RenameCommand, + command: &dyn RenameCommand, files: &[PathBuf], dry: bool, auto_confirm: bool, show_unchanged: bool, ) { - let intents = suggest_renames(files, command); + let intents = command.suggest_renames(files); if dry { print_intents(&intents, show_unchanged); } else { @@ -200,7 +92,7 @@ fn process_command( pub fn run(config: &Config) { process_command( - &config.command, + config.command.deref(), &config.files, config.dry, config.auto_confirm, diff --git a/src/main.rs b/src/main.rs index 1535a5d..eb4fa15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,11 @@ use std::{env, path::PathBuf}; use clap::{arg, command, value_parser, Arg, ArgAction, ArgMatches, Command}; -use muren::{run, Config, RenameCommand}; +use muren::{run, Config}; +use muren::commands::{SetExtension, RenameCommand, Remove, Normalize, FixExtension, Prefix, Replace, ChangeCase}; fn parse_config(matches: &ArgMatches) -> Config { - let command = extract_command(matches).unwrap(); + let command = extract_command(matches); let files_args = matches.subcommand().unwrap().1.get_many::("path"); let files: Vec = match files_args { Some(args) => args.cloned().collect(), @@ -20,29 +21,32 @@ fn parse_config(matches: &ArgMatches) -> Config { } } -fn extract_command(args_matches: &ArgMatches) -> Option { - match args_matches.subcommand() { - Some(("set-ext", matches)) => Some(RenameCommand::SetExtension( - matches.get_one::("extension").unwrap().clone(), - )), - Some(("remove", matches)) => Some(RenameCommand::Remove( - matches.get_one::("pattern").unwrap().clone(), - )), - Some(("normalize", _)) => Some(RenameCommand::Normalize), - Some(("fix-ext", matches)) => Some(RenameCommand::FixExtension(matches.get_flag("append"))), - Some(("prefix", matches)) => Some(RenameCommand::Prefix( - matches.get_one::("prefix").unwrap().clone(), - )), - Some(("replace", matches)) => Some(RenameCommand::Replace( - matches.get_one::("pattern").unwrap().clone(), - matches.get_one::("replacement").unwrap().clone(), - matches.get_flag("regex"), - )), - Some(("change-case", matches)) => { - Some(RenameCommand::ChangeCase(matches.get_flag("upper"))) - } - _ => None, - } +fn extract_command(args_matches: &ArgMatches) -> Box { + match args_matches.subcommand() + { + None => panic!("No command provided"), + Some((m, matches)) => + match m { + "set-ext" => Box::new(SetExtension { + extension: matches.get_one::("extension").unwrap().clone(), + }), + "remove" => Box::new(Remove { + pattern: matches.get_one::("pattern").unwrap().clone(), + }), + "normalize" => Box::new(Normalize), + "fix-ext" => Box::new(FixExtension { append: matches.get_flag("append") }), + "prefix" => Box::new(Prefix { + prefix: matches.get_one::("prefix").unwrap().clone(), + }), + "replace" => Box::new(Replace { + pattern: matches.get_one::("pattern").unwrap().clone(), + replacement: matches.get_one::("replacement").unwrap().clone(), + is_regex: matches.get_flag("regex"), + }), + "change-case" => + Box::new(ChangeCase { upper: matches.get_flag("upper") }), + _ => panic!("Unknown command"), + }} } fn create_cli_command() -> Command { From 7104813dcf62622a1349446866587a3201eae90c Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Fri, 4 Oct 2024 20:10:58 +0200 Subject: [PATCH 3/7] Make clippy happy --- src/commands.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 07bad28..90ba3c1 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,5 @@ use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use colored::Colorize; use regex::Regex; use unidecode::unidecode; @@ -34,7 +34,7 @@ impl Display for RenameIntent { pub trait RenameCommand { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf; + fn suggest_new_name(&self, old_name: &Path) -> PathBuf; fn suggest_renames(&self, files: &[PathBuf]) -> Vec { files @@ -47,7 +47,7 @@ pub trait RenameCommand { pub struct Normalize; impl RenameCommand for Normalize { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { let path_str = old_name.to_string_lossy().to_string(); let new_name = unidecode(&path_str).replace(' ', "_"); //#.to_lowercase(); PathBuf::from(new_name) @@ -59,8 +59,8 @@ pub struct SetExtension { } impl RenameCommand for SetExtension { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { - let mut new_name = old_name.clone(); + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { + let mut new_name = old_name.to_path_buf(); new_name.set_extension(&self.extension); new_name } @@ -71,7 +71,7 @@ pub struct Remove { } impl RenameCommand for Remove { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { let new_name = old_name.to_string_lossy().replace(&self.pattern, ""); PathBuf::from(new_name) } @@ -84,7 +84,7 @@ pub struct Replace { } impl RenameCommand for Replace { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { let path_str = old_name.to_string_lossy().to_string(); let new_name = if self.is_regex { let re = Regex::new(&self.pattern).unwrap(); @@ -101,7 +101,7 @@ pub struct ChangeCase { } impl RenameCommand for ChangeCase { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { let path_str = old_name.to_string_lossy().to_string(); let new_name = match self.upper { true => path_str.to_uppercase(), @@ -116,16 +116,15 @@ pub struct FixExtension { } impl RenameCommand for FixExtension { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { let possible_extensions = find_extensions_from_content(old_name); - let mut new_name = old_name.clone(); + let mut new_name = old_name.to_path_buf(); if !has_correct_extension(old_name, &possible_extensions) { let mut new_extension = possible_extensions[0].clone(); if self.append { - let old_extension = new_name.extension(); - if old_extension.is_some() { + if let Some(old_extension) = new_name.extension() { new_extension.insert(0, '.'); - new_extension.insert_str(0, old_extension.unwrap().to_str().unwrap()) + new_extension.insert_str(0, old_extension.to_str().unwrap()) } } new_name.set_extension(new_extension); @@ -139,7 +138,7 @@ pub struct Prefix { } impl RenameCommand for Prefix { - fn suggest_new_name(&self, old_name: &PathBuf) -> PathBuf { + fn suggest_new_name(&self, old_name: &Path) -> PathBuf { let mut new_name = self.prefix.clone(); new_name.push_str(old_name.to_string_lossy().to_string().as_str()); PathBuf::from(new_name) From 50339f9e44c0bf4d54238d9296a93aa4e6ef9277 Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Fri, 4 Oct 2024 20:41:06 +0200 Subject: [PATCH 4/7] Start unit testing --- src/commands.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/commands.rs b/src/commands.rs index 90ba3c1..b82907f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -5,6 +5,7 @@ use regex::Regex; use unidecode::unidecode; use crate::extensions::{find_extensions_from_content, has_correct_extension}; +#[derive(Clone)] pub struct RenameIntent { pub old_name: PathBuf, pub new_name: PathBuf, @@ -143,4 +144,20 @@ impl RenameCommand for Prefix { new_name.push_str(old_name.to_string_lossy().to_string().as_str()); PathBuf::from(new_name) } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_set_prefix() { + let p = Prefix { prefix: String::from("a") }; + let old_path = PathBuf::from("b"); + assert_eq!( + p.suggest_new_name(&old_path), + PathBuf::from("ab") + ) + } } \ No newline at end of file From ffb1e3e4c6f5ed645fcf8305f31cac349799efff Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Fri, 4 Oct 2024 20:41:17 +0200 Subject: [PATCH 5/7] contains_duplicates --- src/lib.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index a3897c3..cf1a0e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod extensions; pub mod commands; +use std::collections::HashSet; use colored::Colorize; use std::fs::rename; use std::ops::Deref; @@ -66,6 +67,13 @@ fn process_command( show_unchanged: bool, ) { let intents = command.suggest_renames(files); + + if contains_duplicates(&intents) { + print!("All target names are not unique!"); + print_intents(&intents, false); + return; + } + if dry { print_intents(&intents, show_unchanged); } else { @@ -90,6 +98,12 @@ fn process_command( }; } +fn contains_duplicates(intents: &Vec) -> bool { + let new_names: Vec = intents.iter().map(|intent| intent.new_name.clone()).collect(); + let mut uniq = HashSet::new(); + !new_names.into_iter().all(move |x| uniq.insert(x)) +} + pub fn run(config: &Config) { process_command( config.command.deref(), @@ -99,3 +113,19 @@ pub fn run(config: &Config) { config.show_unchanged, ); } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_contains_duplicates() { + let a_to_b = RenameIntent{ old_name: PathBuf::from("a"), new_name: PathBuf::from("b")}; + let b_to_d = RenameIntent{ old_name: PathBuf::from("b"), new_name: PathBuf::from("d")}; + let c_to_d = RenameIntent{ old_name: PathBuf::from("c"), new_name: PathBuf::from("d")}; + + assert!(contains_duplicates(&vec![b_to_d, c_to_d.clone()])); + assert!(!contains_duplicates(&vec![a_to_b, c_to_d])); + assert!(!contains_duplicates(&Vec::new())); + } +} \ No newline at end of file From 4a1b5118c763ef42295b7b039fe77c7fbdeefaf5 Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Fri, 4 Oct 2024 21:47:56 +0200 Subject: [PATCH 6/7] A bit more unit tests --- src/commands.rs | 126 +++++++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 28 ++++++++--- src/main.rs | 54 +++++++++++---------- 3 files changed, 164 insertions(+), 44 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index b82907f..03e6024 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,9 +1,9 @@ -use std::fmt::{Display, Formatter}; -use std::path::{Path, PathBuf}; +use crate::extensions::{find_extensions_from_content, has_correct_extension}; use colored::Colorize; use regex::Regex; +use std::fmt::{Display, Formatter}; +use std::path::{Path, PathBuf}; use unidecode::unidecode; -use crate::extensions::{find_extensions_from_content, has_correct_extension}; #[derive(Clone)] pub struct RenameIntent { @@ -33,14 +33,16 @@ impl Display for RenameIntent { } } - pub trait RenameCommand { fn suggest_new_name(&self, old_name: &Path) -> PathBuf; fn suggest_renames(&self, files: &[PathBuf]) -> Vec { files .iter() - .map(|path| RenameIntent { old_name: path.clone(), new_name: self.suggest_new_name(path) }) + .map(|path| RenameIntent { + old_name: path.clone(), + new_name: self.suggest_new_name(path), + }) .collect() } } @@ -151,13 +153,115 @@ mod tests { use super::*; use std::path::PathBuf; + /// Compare whether old_names are converted to new expected_names using command. + fn assert_renames_correctly( + command: &dyn RenameCommand, + old_names: &[&str], + expected_names: &[&str], + ) { + let old: Vec = old_names.iter().map(|&x| PathBuf::from(x)).collect(); + let new_intents = command.suggest_renames(&old); + let new: Vec = new_intents + .iter() + .map(|intent| intent.new_name.clone()) + .collect(); + let expected: Vec = expected_names.iter().map(|&x| PathBuf::from(x)).collect(); + assert_eq!(expected, new); + } + #[test] fn test_set_prefix() { - let p = Prefix { prefix: String::from("a") }; + let p = Prefix { prefix: String::from("a") }; let old_path = PathBuf::from("b"); - assert_eq!( - p.suggest_new_name(&old_path), - PathBuf::from("ab") - ) + assert_eq!(p.suggest_new_name(&old_path), PathBuf::from("ab")) + } + + mod test_replace { + use super::*; + + #[test] + fn test_regex() { + // Regex really matching + let replace = Replace { + pattern: String::from("\\d"), + replacement: String::from("a"), + is_regex: true, + }; + let old_path = PathBuf::from("a222"); + assert_eq!(replace.suggest_new_name(&old_path), PathBuf::from("aaaa")); + + // Regex present as literal + let replace = Replace { + pattern: String::from("a$"), + replacement: String::from("a"), + is_regex: true, + }; + let old_path = PathBuf::from("a$a"); + assert_eq!(replace.suggest_new_name(&old_path), PathBuf::from("a$a")); + } + + #[test] + fn test_non_regex() { + let command = Replace { + pattern: String::from("a.c"), + replacement: String::from("def"), + is_regex: false, + }; + let old_path = PathBuf::from("a.cabc"); + assert_eq!(command.suggest_new_name(&old_path), PathBuf::from("defabc")); + } } -} \ No newline at end of file + + mod test_change_case { + use super::*; + + #[test] + fn test_upper() { + assert_renames_correctly( + &ChangeCase { upper: true }, + &["Abc", "hnědý", "Αθήνα", "mountAIN🗻"], + &["ABC", "HNĚDÝ", "ΑΘΉΝΑ", "MOUNTAIN🗻"] + ); + } + + #[test] + fn test_lower() { + assert_renames_correctly( + &ChangeCase { upper: false }, + &["Abc", "hnědý", "Αθήνα", "mountAIN🗻"], + &["abc", "hnědý", "αθήνα", "mountain🗻"] + ); + } + } + + #[test] + fn test_normalize() { + assert_renames_correctly( + &Normalize, + &["Abc", "hnědý", "Αθήνα & Σπάρτη", "mountain🗻"], + &["Abc", "hnedy", "Athena_&_Sparte", "mountain"] + ); + } + + mod test_set_extension { + use super::*; + + #[test] + fn test_no_extension() { + assert_renames_correctly( + &SetExtension{ extension: String::from("") }, + &["a", "b", "c.jpg", ".gitignore"], + &["a", "b", "c", ".gitignore"], + ); + } + + #[test] + fn test_some_extension() { + assert_renames_correctly( + &SetExtension{ extension: String::from("jpg") }, + &["a", "b", "c.jpg", ".gitignore"], + &["a.jpg", "b.jpg", "c.jpg", ".gitignore.jpg"], + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index cf1a0e2..844c66c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ -pub mod extensions; pub mod commands; +pub mod extensions; -use std::collections::HashSet; use colored::Colorize; +use std::collections::HashSet; use std::fs::rename; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -98,8 +98,11 @@ fn process_command( }; } -fn contains_duplicates(intents: &Vec) -> bool { - let new_names: Vec = intents.iter().map(|intent| intent.new_name.clone()).collect(); +fn contains_duplicates(intents: &[RenameIntent]) -> bool { + let new_names: Vec = intents + .iter() + .map(|intent| intent.new_name.clone()) + .collect(); let mut uniq = HashSet::new(); !new_names.into_iter().all(move |x| uniq.insert(x)) } @@ -120,12 +123,21 @@ mod test { #[test] fn test_contains_duplicates() { - let a_to_b = RenameIntent{ old_name: PathBuf::from("a"), new_name: PathBuf::from("b")}; - let b_to_d = RenameIntent{ old_name: PathBuf::from("b"), new_name: PathBuf::from("d")}; - let c_to_d = RenameIntent{ old_name: PathBuf::from("c"), new_name: PathBuf::from("d")}; + let a_to_b = RenameIntent { + old_name: PathBuf::from("a"), + new_name: PathBuf::from("b"), + }; + let b_to_d = RenameIntent { + old_name: PathBuf::from("b"), + new_name: PathBuf::from("d"), + }; + let c_to_d = RenameIntent { + old_name: PathBuf::from("c"), + new_name: PathBuf::from("d"), + }; assert!(contains_duplicates(&vec![b_to_d, c_to_d.clone()])); assert!(!contains_duplicates(&vec![a_to_b, c_to_d])); assert!(!contains_duplicates(&Vec::new())); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index eb4fa15..9dc98c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,10 @@ use std::{env, path::PathBuf}; use clap::{arg, command, value_parser, Arg, ArgAction, ArgMatches, Command}; +use muren::commands::{ + ChangeCase, FixExtension, Normalize, Prefix, Remove, RenameCommand, Replace, SetExtension, +}; use muren::{run, Config}; -use muren::commands::{SetExtension, RenameCommand, Remove, Normalize, FixExtension, Prefix, Replace, ChangeCase}; fn parse_config(matches: &ArgMatches) -> Config { let command = extract_command(matches); @@ -22,31 +24,33 @@ fn parse_config(matches: &ArgMatches) -> Config { } fn extract_command(args_matches: &ArgMatches) -> Box { - match args_matches.subcommand() - { + match args_matches.subcommand() { None => panic!("No command provided"), - Some((m, matches)) => - match m { - "set-ext" => Box::new(SetExtension { - extension: matches.get_one::("extension").unwrap().clone(), - }), - "remove" => Box::new(Remove { - pattern: matches.get_one::("pattern").unwrap().clone(), - }), - "normalize" => Box::new(Normalize), - "fix-ext" => Box::new(FixExtension { append: matches.get_flag("append") }), - "prefix" => Box::new(Prefix { - prefix: matches.get_one::("prefix").unwrap().clone(), - }), - "replace" => Box::new(Replace { - pattern: matches.get_one::("pattern").unwrap().clone(), - replacement: matches.get_one::("replacement").unwrap().clone(), - is_regex: matches.get_flag("regex"), - }), - "change-case" => - Box::new(ChangeCase { upper: matches.get_flag("upper") }), - _ => panic!("Unknown command"), - }} + Some((m, matches)) => match m { + "set-ext" => Box::new(SetExtension { + extension: matches.get_one::("extension").unwrap().clone(), + }), + "remove" => Box::new(Remove { + pattern: matches.get_one::("pattern").unwrap().clone(), + }), + "normalize" => Box::new(Normalize), + "fix-ext" => Box::new(FixExtension { + append: matches.get_flag("append"), + }), + "prefix" => Box::new(Prefix { + prefix: matches.get_one::("prefix").unwrap().clone(), + }), + "replace" => Box::new(Replace { + pattern: matches.get_one::("pattern").unwrap().clone(), + replacement: matches.get_one::("replacement").unwrap().clone(), + is_regex: matches.get_flag("regex"), + }), + "change-case" => Box::new(ChangeCase { + upper: matches.get_flag("upper"), + }), + _ => panic!("Unknown command"), + }, + } } fn create_cli_command() -> Command { From a5e148d89097553e7d131b33801f93e054728b4e Mon Sep 17 00:00:00 2001 From: Jan Pipek Date: Fri, 4 Oct 2024 21:55:22 +0200 Subject: [PATCH 7/7] Unit tests for commands --- src/commands.rs | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 03e6024..0636af3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -170,10 +170,12 @@ mod tests { } #[test] - fn test_set_prefix() { - let p = Prefix { prefix: String::from("a") }; - let old_path = PathBuf::from("b"); - assert_eq!(p.suggest_new_name(&old_path), PathBuf::from("ab")) + fn test_prefix() { + assert_renames_correctly( + &Prefix { prefix: String::from("a") }, + &["b", "a"], + &["ab", "aa"] + ); } mod test_replace { @@ -181,23 +183,16 @@ mod tests { #[test] fn test_regex() { - // Regex really matching - let replace = Replace { + let command = Replace { pattern: String::from("\\d"), replacement: String::from("a"), is_regex: true, }; - let old_path = PathBuf::from("a222"); - assert_eq!(replace.suggest_new_name(&old_path), PathBuf::from("aaaa")); - - // Regex present as literal - let replace = Replace { - pattern: String::from("a$"), - replacement: String::from("a"), - is_regex: true, - }; - let old_path = PathBuf::from("a$a"); - assert_eq!(replace.suggest_new_name(&old_path), PathBuf::from("a$a")); + assert_renames_correctly( + &command, + &["222", "abc", "answer_is_42", "\\d2"], + &["aaa", "abc", "answer_is_aa", "\\da"] + ); } #[test] @@ -207,8 +202,11 @@ mod tests { replacement: String::from("def"), is_regex: false, }; - let old_path = PathBuf::from("a.cabc"); - assert_eq!(command.suggest_new_name(&old_path), PathBuf::from("defabc")); + assert_renames_correctly( + &command, + &["a.c", "abc", "ABC"], + &["def", "abc", "ABC"] + ); } } @@ -264,4 +262,13 @@ mod tests { ); } } + + #[test] + fn test_remove() { + assert_renames_correctly( + &Remove{ pattern: String::from("ab") }, + &[".gitignore", "babe", "abABab"], + &[".gitignore", "be", "AB"] + ) + } }