2017-05-12 11:50:03 +02:00
|
|
|
extern crate walkdir;
|
|
|
|
extern crate regex;
|
|
|
|
extern crate getopts;
|
2017-05-12 13:02:20 +02:00
|
|
|
extern crate ansi_term;
|
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;
|
|
|
|
|
|
|
|
use walkdir::{WalkDir, DirEntry, WalkDirIterator};
|
2017-05-12 12:02:25 +02:00
|
|
|
use regex::{Regex, RegexBuilder};
|
2017-05-12 11:50:03 +02:00
|
|
|
use getopts::Options;
|
2017-05-12 13:02:20 +02:00
|
|
|
use ansi_term::Colour;
|
|
|
|
|
2017-05-12 15:44:09 +02:00
|
|
|
struct FdOptions {
|
|
|
|
case_sensitive: bool,
|
|
|
|
search_full_path: bool,
|
|
|
|
follow_links: bool,
|
|
|
|
colored: bool
|
|
|
|
}
|
|
|
|
|
2017-05-12 13:02:20 +02:00
|
|
|
/// Print a search result to the console.
|
2017-05-12 15:44:09 +02:00
|
|
|
fn print_entry(entry: &DirEntry, path_str: &str, config: &FdOptions) {
|
|
|
|
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.
|
2017-05-12 15:44:09 +02:00
|
|
|
fn scan(root: &Path, pattern: &Regex, config: &FdOptions) {
|
|
|
|
let walker = WalkDir::new(root)
|
|
|
|
.follow_links(config.follow_links)
|
2017-05-12 19:34:31 +02:00
|
|
|
.into_iter()
|
|
|
|
.filter_entry(|e| !is_hidden(e))
|
|
|
|
.filter_map(|e| e.ok())
|
|
|
|
.filter(|e| e.path() != root);
|
2017-05-12 15:44:09 +02:00
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Some(path_str) = path_rel.to_str() {
|
|
|
|
let res =
|
|
|
|
if config.search_full_path {
|
|
|
|
pattern.find(path_str)
|
|
|
|
} else {
|
|
|
|
path_rel.file_name()
|
|
|
|
.and_then(OsStr::to_str)
|
|
|
|
.and_then(|s| pattern.find(s))
|
|
|
|
};
|
|
|
|
|
|
|
|
res.map(|_| print_entry(&entry, path_str, &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`.
|
2017-05-12 12:39:13 +02:00
|
|
|
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)");
|
2017-05-12 15:44:09 +02:00
|
|
|
opts.optflag("F", "follow", "follow symlinks (default: off)");
|
|
|
|
opts.optflag("n", "no-color", "do not colorize output");
|
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,
|
2017-05-12 12:39:13 +02:00
|
|
|
Err(e) => error(e.description())
|
2017-05-12 11:50:03 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
if matches.opt_present("h") {
|
|
|
|
let brief = "Usage: fd [PATTERN]";
|
|
|
|
print!("{}", opts.usage(&brief));
|
|
|
|
process::exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
let empty = String::new();
|
|
|
|
let pattern = matches.free.get(0).unwrap_or(&empty);
|
|
|
|
|
2017-05-12 12:39:13 +02:00
|
|
|
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();
|
|
|
|
|
2017-05-12 15:44:09 +02:00
|
|
|
|
|
|
|
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).
|
2017-05-12 15:44:09 +02:00
|
|
|
case_sensitive: matches.opt_present("s") ||
|
|
|
|
pattern.chars().any(char::is_uppercase),
|
2017-05-12 22:20:14 +02:00
|
|
|
search_full_path: !matches.opt_present("f"),
|
2017-05-12 15:44:09 +02:00
|
|
|
colored: !matches.opt_present("n"),
|
|
|
|
follow_links: matches.opt_present("F")
|
|
|
|
};
|
2017-05-12 13:32:30 +02:00
|
|
|
|
2017-05-12 12:39:13 +02:00
|
|
|
match RegexBuilder::new(pattern)
|
2017-05-12 15:44:09 +02:00
|
|
|
.case_insensitive(!config.case_sensitive)
|
2017-05-12 12:39:13 +02:00
|
|
|
.build() {
|
2017-05-12 22:20:14 +02:00
|
|
|
Ok(re) => scan(¤t_dir, &re, &config),
|
2017-05-12 12:39:13 +02:00
|
|
|
Err(err) => error(err.description())
|
2017-05-12 11:50:03 +02:00
|
|
|
}
|
|
|
|
}
|