filenamefix 0.1.4

A tool for fix filename.
Documentation
use std::error::Error;
use std::fs;
use walkdir::{DirEntry, WalkDir};
use regex::Regex;

pub struct Config {
    pub cmd: String,
    pub params: Option<String>,
    pub dirname: String
}

impl Config {
    pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("Not enough arguments");
        }
        args.next();
        let cmd = args.next().unwrap();
        let params_or_dirname = args.next().unwrap();
        match args.next() {
            Some(arg) => Ok(Config{ cmd, params: Some(params_or_dirname), dirname: arg }),
            None => Ok(Config{ cmd, params: None, dirname: params_or_dirname})
        }

        
    }
}

pub fn run (config: Config) -> Result<(), Box<dyn Error>> {
    if config.cmd.eq("number") {
        fix_number(&config.dirname, &config.params)?
    } else if config.cmd.eq("space") {
        fix_space(&config.dirname, &config.params)?
    } else if config.cmd.eq("ext") {
        fix_ext(&config.dirname, &config.params)?
    } else {
        return Err("Unknown CMD".into());
    }
    Ok(())
}
fn is_hidden(entry: &DirEntry) -> bool {
    entry.file_name()
        .to_str()
        .map(|s| s.starts_with("."))
        .unwrap_or(false)
}
fn starts_with_number(entry: &DirEntry) -> bool {
    entry.file_name()
    .to_str()
    .map(|s| s.chars().next().unwrap().is_numeric())
    .unwrap_or(false) 
}
fn fix_number(dirname: &str, params: &Option<String>) -> std::io::Result<()> {
    let number = match params {
        Some(params) => match params.trim().parse() {
            Ok(num) => num,
            Err(_) => 2,
        },
        None => 2
    };
    for entry in WalkDir::new(dirname)
            .into_iter()
            .filter_map(Result::ok)
            .filter(|e| !e.file_type().is_dir() && !is_hidden(e) && starts_with_number(e)) {
        let f_name = String::from(entry.file_name().to_string_lossy());

        let re = Regex::new(r"^([0-9]+)\W*(.+)$").unwrap();
        let caps = re.captures(&f_name).unwrap();
        let f_name_prefix = caps.get(1).map_or("", |m| m.as_str());
        let f_name_suffix = caps.get(2).map_or("", |m| m.as_str());
        let o_number: i32 = f_name_prefix.parse().unwrap();
        
        let new_number = format!("{:0alignment$}", o_number, alignment = number);
        let new_f_name = format!("{}.{}", new_number, f_name_suffix);
        let path = entry.into_path();
        let mut new_path = path.clone();
        new_path.set_file_name(new_f_name);
        fs::rename(path, new_path)?;
    }
    Ok(())
}

fn fix_space(dirname: &str, params: &Option<String>) -> std::io::Result<()> {
    let divider = match params {
        Some(params) => params,
        None => "_"
    };
    for entry in WalkDir::new(dirname)
            .into_iter()
            .filter_map(Result::ok)
            .filter(|e| !e.file_type().is_dir() && !is_hidden(e)) {
        let f_name = String::from(entry.file_name().to_string_lossy());
        let re = Regex::new(r"(.+?)(\.[^.]*$|$)").unwrap();
        let caps = re.captures(&f_name).unwrap();
        let f_name_without_extension = caps.get(1).map_or("", |m| m.as_str()).trim();
        let parts: Vec<&str> = f_name_without_extension.split_whitespace().collect();
        let new_f_name_without_extension:String = parts.join(divider);
        let extension = caps.get(2).map_or("", |m| m.as_str());
        let new_f_name = format!("{}{}", new_f_name_without_extension, extension);
        let path = entry.into_path();
        let mut new_path = path.clone();
        new_path.set_file_name(new_f_name);
        fs::rename(path, new_path)?;
    }
    Ok(())
}
fn fix_ext(dirname: &str, params: &Option<String>) -> std::io::Result<()> {
    let upper = match params {
        Some(params) => params == "upper",
        None => false
    };
    for entry in WalkDir::new(dirname)
            .into_iter()
            .filter_map(Result::ok)
            .filter(|e| !e.file_type().is_dir() && !is_hidden(e)) {
        let f_name = String::from(entry.file_name().to_string_lossy());
        let re = Regex::new(r"(.+?)(\.[^.]*$|$)").unwrap();
        let caps = re.captures(&f_name).unwrap();
        let f_name_without_extension = caps.get(1).map_or("", |m| m.as_str()).trim();
        let extension = caps.get(2).map_or("", |m| m.as_str());
        if !extension.eq("") {
            let new_extension = if upper { extension.to_uppercase() } else { extension.to_lowercase() };
            let new_f_name = format!("{}{}", f_name_without_extension, new_extension);
            let path = entry.into_path();
            let mut new_path = path.clone();
            new_path.set_file_name(new_f_name);
            fs::rename(path, new_path)?;
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn fix_filename_number() {
        let dirname = "./src/testdir";
        let params = "2";
        assert_eq!((), fix_number(&dirname, &Some(params.to_string())).unwrap());
    }
    #[test]
    fn fix_filename_space() {
        let dirname = "./src/testdir";
        let params = "-";
        assert_eq!((), fix_space(&dirname, &Some(params.to_string())).unwrap());
    }

    #[test]
    fn fix_filename_ext() {
        let dirname = "./src/testdir";
        let params = "lower";
        assert_eq!((), fix_ext(&dirname, &Some(params.to_string())).unwrap());
    }
}