Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

split-modules #18

Merged
merged 7 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
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;

#[derive(Clone)]
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: &Path) -> PathBuf;

fn suggest_renames(&self, files: &[PathBuf]) -> Vec<RenameIntent> {
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: &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)
}
}

pub struct SetExtension {
pub extension: String,
}

impl RenameCommand for SetExtension {
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
}
}

pub struct Remove {
pub pattern: String,
}

impl RenameCommand for Remove {
fn suggest_new_name(&self, old_name: &Path) -> 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: &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();
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: &Path) -> 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: &Path) -> PathBuf {
let possible_extensions = find_extensions_from_content(old_name);
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 {
if let Some(old_extension) = new_name.extension() {
new_extension.insert(0, '.');
new_extension.insert_str(0, old_extension.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: &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)
}
}

#[cfg(test)]
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<PathBuf> = old_names.iter().map(|&x| PathBuf::from(x)).collect();
let new_intents = command.suggest_renames(&old);
let new: Vec<PathBuf> = new_intents
.iter()
.map(|intent| intent.new_name.clone())
.collect();
let expected: Vec<PathBuf> = expected_names.iter().map(|&x| PathBuf::from(x)).collect();
assert_eq!(expected, new);
}

#[test]
fn test_prefix() {
assert_renames_correctly(
&Prefix { prefix: String::from("a") },
&["b", "a"],
&["ab", "aa"]
);
}

mod test_replace {
use super::*;

#[test]
fn test_regex() {
let command = Replace {
pattern: String::from("\\d"),
replacement: String::from("a"),
is_regex: true,
};
assert_renames_correctly(
&command,
&["222", "abc", "answer_is_42", "\\d2"],
&["aaa", "abc", "answer_is_aa", "\\da"]
);
}

#[test]
fn test_non_regex() {
let command = Replace {
pattern: String::from("a.c"),
replacement: String::from("def"),
is_regex: false,
};
assert_renames_correctly(
&command,
&["a.c", "abc", "ABC"],
&["def", "abc", "ABC"]
);
}
}

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"],
);
}
}

#[test]
fn test_remove() {
assert_renames_correctly(
&Remove{ pattern: String::from("ab") },
&[".gitignore", "babe", "abABab"],
&[".gitignore", "be", "AB"]
)
}
}
81 changes: 81 additions & 0 deletions src/extensions.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
// 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<String> {
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)
}
}
}
Loading
Loading