simino 1.0.0

Batch rename utility for developers
Documentation
use anyhow::{ensure, Result};
use colored::{self, Colorize};
use is_terminal::IsTerminal;
use simino::cli::{Cli, Order};
use simino::errors::SourceError;
use simino::input::{Formatter, InputIterator, Source};
use prettytable::{format, row, Table};
use serde_json::map::Map;
use serde_json::value::Value;
use std::env::{args, set_current_dir};
use std::fs;
use std::path::Path;
use std::process::exit;

fn read_source(
    regex: Option<(&str, Option<usize>, Option<usize>)>,
    sort: Option<Order>,
    map: Option<&Path>,
) -> Result<Source> {
    match (regex, sort, map) {
        (Some((pattern, depth, max_depth)), _, _) => Source::new_regex(pattern, depth, max_depth),
        (_, Some(order), _) => Source::new_sort(order),
        (_, _, Some(filename)) => Source::new_map(filename),
        _ => {
            colored::control::set_override(std::io::stderr().is_terminal());
            Err(SourceError::new(format!(
                "one of '{}', '{}', '{}' or '{}' options must be set.\n{}: run '{} {}' for more information.",
                "regex".cyan(),
                "sort".cyan(),
                "map".cyan(),
                "SOURCE".cyan(),
                "usage".yellow().bold(),
                args().next().unwrap().cyan(),
                "--help".cyan(),
            )).into())
        }
    }
}

fn read_output(output: Option<&str>) -> Result<Option<Formatter>> {
    if output.is_none() {
        return Ok(None);
    }
    Ok(Some(Formatter::new(output.unwrap())?))
}

fn rename_files(
    input_iter: InputIterator,
    test_mode: bool,
    need_map: bool,
    overwrite: bool,
    mkdir: bool,
) -> (Option<Map<String, Value>>, bool) {
    let mut map = if need_map { Some(Map::new()) } else { None };
    let mut is_renamed = true;
    let mut with_err = false;
    for (input, mut output) in input_iter {
        if input.as_str() == output.as_str() {
            map.as_mut().map(|m| m.insert(output, Value::String(input)));
            continue;
        }
        let mut file_path_buf;
        let mut file_path = Path::new(output.as_str());
        if !overwrite {
            while file_path.exists() {
                file_path_buf = file_path
                    .with_file_name(
                        (String::from("_")
                            + file_path
                                .file_name()
                                .and_then(|name| name.to_str())
                                .unwrap_or_default())
                        .as_str(),
                    )
                    .to_path_buf();
                file_path = file_path_buf.as_path();
                output = file_path.to_string_lossy().to_string();
            }
        }
        if mkdir {
            let _ = file_path
                .parent()
                .and_then(|parent| fs::create_dir_all(parent).ok());
        }
        if !test_mode {
            match fs::rename(input.as_str(), file_path) {
                Ok(_) => is_renamed = true,
                Err(e) => {
                    is_renamed = false;
                    with_err = true;
                    eprintln!(
                        "[{}] unable to rename '{}': {}",
                        "error".red().bold(),
                        input.as_str(),
                        e
                    );
                }
            }
        }
        if is_renamed && need_map {
            map.as_mut().map(|m| m.insert(output, Value::String(input)));
        }
        is_renamed = true;
    }
    (map, with_err)
}

fn print_map_table(map: Map<String, Value>) {
    let mut table = Table::new();
    table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
    table.set_titles(row!["Input".cyan(), "Output".cyan()]);
    map.into_iter()
        .enumerate()
        .for_each(|(i, (output, input))| {
            if let Value::String(input) = input {
                if i % 2 == 0 {
                    table.add_row(row![input.as_str(), output.as_str()]);
                } else {
                    table.add_row(row![input.as_str().purple(), output.as_str().purple()]);
                }
            }
        });
    table.printstd();
}

fn run_app() -> Result<bool> {
    let mut opts = Cli::parse();
    if let Some(cwd) = opts.directory {
        set_current_dir(cwd)?;
    }

    ensure!(
        (opts.regex.is_none() && opts.sort.is_none() && opts.map.is_none())
            || opts.output.len() <= 1,
        "optional SOURCE must be used without setting regex, map or sort flags",
    );

    let output = opts.output.pop();
    let pattern = opts.output.pop();
    let input_iter = InputIterator::new(
        read_source(
            opts.regex
                .or(pattern)
                .as_deref()
                .map(|pattern| (pattern, opts.depth, opts.max_depth)),
            opts.sort,
            opts.map.as_deref(),
        )?,
        read_output(output.as_deref())?,
        opts.extension,
    )?;
    let (map, with_err) = rename_files(
        input_iter,
        opts.test,
        opts.print || opts.generate.is_some(),
        opts.overwrite,
        opts.mkdir,
    );
    if let Some(map_file) = opts.generate {
        fs::write(
            map_file,
            serde_json::to_vec_pretty(map.as_ref().unwrap())?.as_slice(),
        )?;
    }
    if let Some(map) = map.filter(|map| opts.print && !map.is_empty()) {
        colored::control::set_override(std::io::stdout().is_terminal());
        print_map_table(map);
    }
    Ok(with_err)
}

fn main() {
    exit(match run_app() {
        Ok(with_err) => i32::from(with_err),
        Err(err) => {
            colored::control::set_override(std::io::stderr().is_terminal());
            eprintln!("{}: {}", "error".red().bold(), err);
            1
        }
    });
}