fd/src/main.rs

161 lines
5 KiB
Rust
Raw Normal View History

2017-05-12 13:02:20 +02:00
extern crate ansi_term;
extern crate getopts;
extern crate isatty;
extern crate regex;
extern crate walkdir;
2017-05-12 11:50:03 +02:00
use std::env;
2017-05-12 19:34:31 +02:00
use std::error::Error;
use std::ffi::OsStr;
2017-05-12 11:50:03 +02:00
use std::io::Write;
2017-05-12 19:34:31 +02:00
use std::path::Path;
2017-05-12 11:50:03 +02:00
use std::process;
2017-05-12 13:02:20 +02:00
use ansi_term::Colour;
use getopts::Options;
use isatty::stdout_isatty;
use regex::{Regex, RegexBuilder};
use walkdir::{WalkDir, DirEntry, WalkDirIterator};
2017-05-12 13:02:20 +02:00
2017-05-14 17:19:45 +02:00
/// Configuration options for *fd*.
struct FdOptions {
case_sensitive: bool,
search_full_path: bool,
search_hidden: bool,
follow_links: bool,
2017-05-12 22:44:06 +02:00
colored: bool,
max_depth: usize
}
2017-05-14 17:19:45 +02:00
/// The default maximum recursion depth.
2017-05-12 23:37:09 +02:00
const MAX_DEPTH_DEFAULT : usize = 25;
2017-05-12 13:02:20 +02:00
/// Print a search result to the console.
2017-05-12 23:23:57 +02:00
fn print_entry(entry: &DirEntry, path_rel: &Path, config: &FdOptions) {
let path_str = match path_rel.to_str() {
Some(p) => p,
None => return
};
if config.colored {
let style = match entry {
e if e.path_is_symbolic_link() => Colour::Purple,
e if e.path().is_dir() => Colour::Cyan,
_ => Colour::White
};
println!("{}", style.paint(path_str));
} else {
println!("{}", path_str);
}
2017-05-12 13:02:20 +02:00
}
2017-05-12 11:50:03 +02:00
2017-05-12 12:02:25 +02:00
/// Check if filename of entry starts with a dot.
2017-05-12 11:50:03 +02:00
fn is_hidden(entry: &DirEntry) -> bool {
entry.file_name()
.to_str()
.map(|s| s.starts_with("."))
.unwrap_or(false)
}
2017-05-12 19:34:31 +02:00
/// Recursively scan the given root path and search for files / pathnames
/// matching the pattern.
fn scan(root: &Path, pattern: &Regex, config: &FdOptions) {
let walker = WalkDir::new(root)
.follow_links(config.follow_links)
2017-05-12 22:44:06 +02:00
.max_depth(config.max_depth)
2017-05-12 19:34:31 +02:00
.into_iter()
.filter_entry(|e| config.search_hidden || !is_hidden(e))
2017-05-12 19:34:31 +02:00
.filter_map(|e| e.ok())
.filter(|e| e.path() != root);
2017-05-12 19:34:31 +02:00
for entry in walker {
2017-05-12 22:20:14 +02:00
let path_rel = match entry.path().strip_prefix(root) {
Ok(p) => p,
Err(_) => continue
};
2017-05-12 23:23:57 +02:00
let search_str =
if config.search_full_path {
path_rel.to_str()
} else {
if !path_rel.is_file() { continue }
2017-05-12 23:23:57 +02:00
path_rel.file_name()
.and_then(OsStr::to_str)
};
2017-05-12 22:20:14 +02:00
2017-05-12 23:23:57 +02:00
search_str.and_then(|s| pattern.find(s))
.map(|_| print_entry(&entry, path_rel, &config));
2017-05-12 11:50:03 +02:00
}
}
2017-05-12 12:02:25 +02:00
/// Print error message to stderr and exit with status `1`.
fn error(message: &str) -> ! {
writeln!(&mut std::io::stderr(), "{}", message)
2017-05-12 11:50:03 +02:00
.expect("Failed writing to stderr");
process::exit(1);
}
fn main() {
let args: Vec<String> = env::args().collect();
let mut opts = Options::new();
opts.optflag("h", "help", "print this help message");
2017-05-12 22:20:14 +02:00
opts.optflag("s", "sensitive",
"case-sensitive search (default: smart case)");
opts.optflag("f", "filename",
"search filenames only (default: full path)");
opts.optflag("H", "hidden",
"search hidden files/directories (default: off)");
opts.optflag("F", "follow", "follow symlinks (default: off)");
2017-05-12 23:37:09 +02:00
opts.optflag("n", "no-color", "do not colorize output (default: on)");
opts.optopt("d", "max-depth",
2017-05-14 17:19:45 +02:00
format!("maximum search depth (default: {})",
MAX_DEPTH_DEFAULT).as_str(),
"D");
2017-05-12 19:34:31 +02:00
2017-05-12 11:50:03 +02:00
let matches = match opts.parse(&args[1..]) {
2017-05-12 22:20:14 +02:00
Ok(m) => m,
Err(e) => error(e.description())
2017-05-12 11:50:03 +02:00
};
if matches.opt_present("h") {
2017-05-12 23:37:09 +02:00
let brief = "Usage: fd [options] [PATTERN]";
2017-05-12 11:50:03 +02:00
print!("{}", opts.usage(&brief));
process::exit(1);
}
let empty = String::new();
let pattern = matches.free.get(0).unwrap_or(&empty);
let current_dir_buf = match env::current_dir() {
Ok(cd) => cd,
Err(_) => error("Could not get current directory!")
};
2017-05-12 11:50:03 +02:00
let current_dir = current_dir_buf.as_path();
let config = FdOptions {
2017-05-12 22:20:14 +02:00
// The search will be case-sensitive if the command line flag is set or
// if the pattern has an uppercase character (smart case).
case_sensitive: matches.opt_present("sensitive") ||
pattern.chars().any(char::is_uppercase),
search_full_path: !matches.opt_present("filename"),
search_hidden: matches.opt_present("hidden"),
colored: !matches.opt_present("no-color") &&
stdout_isatty(),
2017-05-12 22:44:06 +02:00
follow_links: matches.opt_present("follow"),
max_depth:
matches.opt_str("max-depth")
.and_then(|ds| usize::from_str_radix(&ds, 10).ok())
2017-05-12 23:37:09 +02:00
.unwrap_or(MAX_DEPTH_DEFAULT)
};
2017-05-12 13:32:30 +02:00
match RegexBuilder::new(pattern)
.case_insensitive(!config.case_sensitive)
.build() {
2017-05-12 22:20:14 +02:00
Ok(re) => scan(&current_dir, &re, &config),
Err(err) => error(err.description())
2017-05-12 11:50:03 +02:00
}
}