ecd 0.1.1

Fast CLI to detect text file encodings (UTF-8, GBK, Shift_JIS, and 35+ more)
Documentation
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};
use globset::{Glob, GlobSetBuilder};

use crate::cli::CheckArgs;

pub fn collect_paths(args: &CheckArgs) -> Result<Vec<PathBuf>> {
    if args.files.is_empty() && args.dirs.is_empty() {
        anyhow::bail!("no input: provide at least one --file (-f) or --directory (-d)");
    }

    let pattern = build_globset(&args.pattern)?;
    let excludes = args.effective_excludes();
    let mut paths = Vec::new();

    for file in &args.files {
        let path = file.clone();
        if !path.exists() {
            anyhow::bail!("path not found: {}", path.display());
        }
        if path.is_file() && matches_pattern(&pattern, &path) {
            paths.push(path);
        }
    }

    for dir in &args.dirs {
        let path = dir.clone();
        if !path.exists() {
            anyhow::bail!("path not found: {}", path.display());
        }
        if path.is_file() {
            if matches_pattern(&pattern, &path) {
                paths.push(path);
            }
            continue;
        }
        walk_dir(&path, &pattern, &excludes, &mut paths)?;
    }

    paths.sort();
    paths.dedup();
    Ok(paths)
}

fn build_globset(pattern: &str) -> Result<globset::GlobSet> {
    let mut builder = GlobSetBuilder::new();
    builder.add(Glob::new(pattern).context("invalid glob pattern")?);
    builder.build().context("failed to compile glob pattern")
}

fn matches_pattern(set: &globset::GlobSet, path: &Path) -> bool {
    set.is_match(path) || set.is_match(path.file_name().unwrap_or_default())
}

fn walk_dir(
    root: &Path,
    pattern: &globset::GlobSet,
    excludes: &[String],
    out: &mut Vec<PathBuf>,
) -> Result<()> {
    let mut builder = ignore::WalkBuilder::new(root);
    builder.hidden(false);
    builder.git_ignore(true);
    builder.git_global(true);
    builder.git_exclude(true);
    builder.ignore(true);

    for name in excludes {
        let name = name.clone();
        builder.filter_entry(move |entry| {
            if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
                entry.file_name().to_string_lossy() != name
            } else {
                true
            }
        });
    }

    for result in builder.build() {
        let entry = result.context("directory walk failed")?;
        if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
            continue;
        }
        let path = entry.into_path();
        if matches_pattern(pattern, &path) {
            out.push(path);
        }
    }

    Ok(())
}