dirwalk 1.0.0

Platform-optimized recursive directory walker with metadata
Documentation
use clap::Parser;
use dirwalk::output::{self, Format, GroupBy};
use dirwalk::{Sort, Threads, WalkBuilder};
use std::io::{self, BufWriter, Write};
use std::process;

#[derive(Parser)]
#[command(
    name = "dirwalk",
    about = "Platform-optimized recursive directory walker"
)]
struct Cli {
    /// Directory to walk
    #[arg(default_value = ".")]
    path: String,

    #[arg(long)]
    max_depth: Option<u32>,

    /// Include hidden files
    #[arg(long)]
    hidden: bool,

    /// Respect .gitignore rules
    #[arg(long)]
    gitignore: bool,

    /// Follow symbolic links
    #[arg(long)]
    follow_links: bool,

    /// Filter by extensions (comma-separated)
    #[arg(long, value_delimiter = ',')]
    extensions: Option<Vec<String>>,

    /// Filter by glob pattern
    #[arg(long)]
    glob: Option<String>,

    /// Minimum file size in bytes
    #[arg(long)]
    min_size: Option<u64>,

    /// Maximum file size in bytes
    #[arg(long)]
    max_size: Option<u64>,

    #[arg(long, value_enum)]
    sort: Option<Sort>,

    /// Sort directories before files
    #[arg(long)]
    dirs_first: bool,

    #[arg(long, value_enum)]
    group_by: Option<GroupBy>,

    /// Print summary statistics
    #[arg(long)]
    stats: bool,

    #[arg(long, value_enum, default_value = "plain")]
    format: Format,

    /// Threads for parallel directory scanning.
    /// Use 0 for all cores, a number like '4', or a fraction like '1/2'.
    #[arg(long, default_value = "0")]
    threads: Threads,
}

fn main() {
    let cli = Cli::parse();

    let mut builder = WalkBuilder::new(&cli.path)
        .hidden(cli.hidden)
        .follow_links(cli.follow_links)
        .gitignore(cli.gitignore)
        .stats(cli.stats)
        .dirs_first(cli.dirs_first)
        .threads(cli.threads);

    if let Some(depth) = cli.max_depth {
        builder = builder.max_depth(depth);
    }
    if let Some(ref exts) = cli.extensions {
        builder = builder.extensions(exts);
    }
    if let Some(ref pattern) = cli.glob {
        builder = match builder.glob(pattern) {
            Ok(b) => b,
            Err(e) => {
                eprintln!("invalid glob pattern: {}", e);
                process::exit(1);
            }
        };
    }
    if let Some(size) = cli.min_size {
        builder = builder.min_size(size);
    }
    if let Some(size) = cli.max_size {
        builder = builder.max_size(size);
    }
    if let Some(sort) = cli.sort {
        builder = builder.sort(sort);
    }

    let result = match builder.build() {
        Ok(r) => r,
        Err(e) => {
            eprintln!("error: {}", e);
            process::exit(1);
        }
    };

    let stdout = io::stdout().lock();
    let mut w = BufWriter::new(stdout);

    if let Err(e) = output::write_entries(&mut w, &result.entries, cli.format, cli.group_by) {
        eprintln!("output error: {}", e);
        process::exit(1);
    }

    if let Some(ref stats) = result.stats
        && let Err(e) = output::write_stats(&mut w, stats)
    {
        eprintln!("output error: {}", e);
        process::exit(1);
    }

    if let Err(e) = w.flush() {
        // Broken pipe (e.g., piped to `head`) is normal — exit silently.
        if e.kind() != std::io::ErrorKind::BrokenPipe {
            eprintln!("output error: {}", e);
            process::exit(1);
        }
    }
}