bfind 0.0.2

A GNU find-like tool, but uses breadth-first search instead of depth-first search
use std::collections::VecDeque;
use std::env;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::thread;
use thiserror::Error;

mod path_queue;
use path_queue::PathQueue;

#[derive(Error, Debug)]
enum Error {
    #[error("std::io::Error {{ kind = {} }}: {source}", source.kind())]
    Io {
        #[from]
        source: io::Error
    },

    #[error("path_queue::Error: {source}")]
    PathQueue {
        #[from]
        source: path_queue::Error
    },
}

type Result<T> = std::result::Result<T, Error>;

struct Options {
    allow_hidden:       bool,
    follow_links:       bool,
    max_depth:          i32,
    ignores:            Vec<String>,
    strip_cwd_prefix:   bool,
}

impl Options {
    pub fn new() -> Self {
        Self {
            allow_hidden: false,
            follow_links: false,
            max_depth: i32::MAX,
            ignores: Vec::new(),
            strip_cwd_prefix: false,
        }
    }
}

fn pop_or_steal(queues: &[PathQueue], index: usize) -> Result<Option<PathBuf>> {
    if let Some(path) = queues[index].pop()? {
        Ok(Some(path))
    } else {
        for (i, queue) in queues.iter().enumerate() {
            if i != index {
                if let Some(path) = queue.pop()? {
                    return Ok(Some(path));
                }
            }
        }
        Ok(None)
    }
}

fn push(queues: &[PathQueue], index: usize, path: PathBuf) -> Result<()> {
    if let Some(mut path) = queues[index].push(path)? {
        loop {
            for (i, queue) in queues.iter().enumerate() {
                if i != index {
                    if let Some(p) = queue.push(path)? {
                        path = p;
                    } else {
                        return Ok(());
                    }
                }
            }
            if let Some(p) = queues[index].push(path)? {
                path = p;
            } else {
                return Ok(());
            }
        }
    }
    Ok(())
}

fn breadth_first_traverse(prog: &str, cwd: &Path, opt: &Options, queues: &[PathQueue], index: usize, counter: &AtomicUsize) -> Result<()> {
    loop {
        let path = pop_or_steal(queues, index)?;
        if let Some(path) = path {
            let entries = fs::read_dir(&path);
            if let Ok(entries) = entries {
                for entry in entries {
                    if let Ok(entry) = entry {
                        let mut path = entry.path();
                        if opt.follow_links && path.is_symlink() {
                            let p = path.read_link();
                            if let Ok(p) = p {
                                path = p;
                            } else {
                                eprintln!("{}: {}: {}", prog, path.display(), p.unwrap_err());
                                continue;
                            }
                        }
                        if let Some(file_name) = entry.file_name().to_str() {
                            if !opt.allow_hidden {
                                if let Some(first_char) = file_name.chars().next() {
                                    if first_char == '.' {
                                        continue;
                                    }
                                }
                            }
                            if opt.ignores.iter().any(|item| item == file_name) {
                                continue;
                            }
                            if opt.strip_cwd_prefix {
                                if path.starts_with("./") {
                                    println!("{}", unsafe { path.strip_prefix("./").unwrap_unchecked() }.display());
                                } else if path.starts_with(cwd) {
                                    println!("{}", unsafe { path.strip_prefix(cwd).unwrap_unchecked() }.display());
                                } else {
                                    println!("{}", path.display());
                                }
                            } else {
                                println!("{}", path.display());
                            }
                            if path.is_dir() {
                                push(queues, index, path)?;
                                counter.fetch_add(1, Ordering::Release);
                            }
                        } else {
                            eprintln!("{}: {}: cannot read filename", prog, path.display());
                        }
                    } else {
                        eprintln!("{}: {}: {}", prog, path.display(), entry.unwrap_err());
                    }
                }
            } else {
                eprintln!("{}: {}: {}", prog, path.display(), entries.unwrap_err());
            }
            counter.fetch_sub(1, Ordering::Release);
        } else if counter.load(Ordering::Acquire) == 0 {
            break;
        } else {
            thread::sleep(Duration::from_millis(7));
        }
    }
    Ok(())
}

#[derive(PartialEq, Eq)]
enum Verb {
    Print,
    Exec
}

#[derive(PartialEq, Eq)]
enum CliState {
    Options,
    Action,
    Expr,
}

fn print_help(prog: &str) {
    println!("{}: [-H] [-L] [-d DEPTH] [-I IGNORE] [DIR ...] [VERB ...] [-- EXPR ...]", prog);
    exit(0);
}

fn main() {
    let mut args: VecDeque<String> = env::args().collect();

    let mut state = CliState::Options;
    let mut roots: Vec<String> = Vec::new();
    let mut opts = Options::new();
    let mut verb = Verb::Print;
    let mut action_tokens = Vec::new();
    let mut expr_tokens: Vec<String> = Vec::new();

    let prog_path = args.pop_front().unwrap();
    let prog = prog_path.rsplit('/').nth_back(0).unwrap();
    let cwd = env::current_dir().unwrap_or_else(|e| {
        eprintln!("{}: {}", prog, e);
        exit(1);
    });

    while let Some(arg) = args.pop_front() {
        match state {
            CliState::Options => {
                if arg == "-H" {
                    opts.allow_hidden = true;
                } else if arg == "-L" {
                    opts.follow_links = true;
                } else if arg == "-h" || arg == "--help" {
                    print_help(prog);
                } else if arg == "-d" || arg == "--depth" {
                    if let Some(depth_str) = args.pop_front() {
                        if let Ok(depth) = depth_str.parse::<i32>() {
                            if depth < 1 {
                                eprintln!("{}: depth must be > 0", prog);
                                exit(1);
                            }
                            opts.max_depth = depth;
                        } else {
                            eprintln!("{}: unable to parse \"{}\" as i32", prog, &depth_str);
                            exit(1);
                        }
                    } else {
                        eprintln!("{}: missing argument to -d", prog);
                        exit(1);
                    }
                } else if arg == "-I" || arg == "--ignore" {
                    if let Some(ignore) = args.pop_front() {
                        opts.ignores = ignore.split(',').map(|s| { s.to_string() }).collect();
                    } else {
                        eprintln!("{}: missing argument to -I", prog);
                        exit(1);
                    }
                } else if arg == "--strip-cwd-prefix" {
                    opts.strip_cwd_prefix = true;
                } else if arg == "print" {
                    verb = Verb::Print;
                    state = CliState::Action;
                } else if arg == "exec" {
                    verb = Verb::Exec;
                    state = CliState::Action;
                } else if arg == "--" {
                    state = CliState::Expr;
                } else if arg.starts_with('-') {
                    eprintln!("{}: unrecognized argument: {}", prog, &arg);
                    exit(1);
                } else {
                    roots.push(arg);
                }
            },
            CliState::Action => {
                if arg == "--" {
                    state = CliState::Expr;
                } else {
                    action_tokens.push(arg);
                }
            },
            CliState::Expr => {
                expr_tokens.push(arg);
            },
        }
    }

    let num_threads = {
        if let Ok(n) = thread::available_parallelism() {
            n.get() + 1
        } else {
            exit(1);
        }
    };

    let mut queues = Vec::new();
    for _ in 0..num_threads {
        let q = PathQueue::new((1024 * 512 / num_threads) as u32, (1024 * 512 / num_threads) as u32);
        if let Ok(q) = q {
            queues.push(q);
        } else {
            eprintln!("{}: {}", prog, q.unwrap_err());
            exit(1);
        }
    }

    let mut counter: usize = 0;

    if roots.is_empty() {
        if let Err(e) = queues[0].push(PathBuf::from(".")) {
            eprintln!("{}: {}", prog, e);
            exit(1);
        }
        counter = 1;
    } else {
        let dotdir = Path::new(".");
        let dotdotdir = Path::new("..");
        let rootdir = Path::new("/");
        for root in &roots {
            let path = PathBuf::from(root);
            if !opts.follow_links && path.is_symlink() {
                continue;
            }
            if path != dotdir && path != dotdotdir && path != rootdir {
                eprintln!("{}", path.display());
                if let Some(file_name) = path.file_name() {
                    if let Some(file_name) = file_name.to_str() {
                        if !opts.allow_hidden {
                            if let Some(first_char) = file_name.chars().next() {
                                if first_char == '.' {
                                    continue;
                                }
                            }
                        }
                        if opts.ignores.iter().any(|item| item == file_name) {
                            continue;
                        }
                    } else {
                        eprintln!("{}: {}: cannot read filename", prog, path.display())
                    }
                } else {
                    unreachable!("path ends with \"..\", which should not happen");
                }
            }
            if let Err(e) = queues[0].push(path) {
                eprintln!("{}: {}", prog, e);
                exit(1)
            } else {
                counter += 1;
            }
        }
    }

    let counter = AtomicUsize::new(counter);
    if let Err(e) = thread::scope(|s| -> Result<()> {
        let cwd = &cwd;
        let opts = &opts;
        let queues = &queues;
        let counter = &counter;
        for i in 0..num_threads {
            s.spawn(move|| -> Result<()> {
                breadth_first_traverse(prog, cwd, opts, queues, i, counter)
            });
        }
        Ok(())
    }) {
        eprintln!("{}: {}", prog, e);
    }
}