2017-05-12 13:02:20 +02:00
|
|
|
extern crate ansi_term;
|
2017-05-12 22:50:52 +02:00
|
|
|
extern crate getopts;
|
|
|
|
extern crate isatty;
|
|
|
|
extern crate regex;
|
|
|
|
extern crate walkdir;
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-05-14 19:49:12 +02:00
|
|
|
use std::collections::HashMap;
|
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-14 19:49:12 +02:00
|
|
|
use std::fs::File;
|
|
|
|
use std::io::{Write, BufReader,BufRead};
|
2017-05-14 17:53:14 +02:00
|
|
|
use std::path::{Path, Component};
|
2017-05-12 11:50:03 +02:00
|
|
|
use std::process;
|
|
|
|
|
2017-05-14 20:49:08 +02:00
|
|
|
use ansi_term::{Style, Colour};
|
2017-05-12 22:50:52 +02:00
|
|
|
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 19:49:12 +02:00
|
|
|
/// Maps file extensions to ANSI colors / styles.
|
|
|
|
type ExtensionStyles = HashMap<String, ansi_term::Style>;
|
|
|
|
|
2017-05-14 17:19:45 +02:00
|
|
|
/// Configuration options for *fd*.
|
2017-05-12 15:44:09 +02:00
|
|
|
struct FdOptions {
|
|
|
|
case_sensitive: bool,
|
|
|
|
search_full_path: bool,
|
2017-05-12 22:29:44 +02:00
|
|
|
search_hidden: bool,
|
2017-05-12 15:44:09 +02:00
|
|
|
follow_links: bool,
|
2017-05-12 22:44:06 +02:00
|
|
|
colored: bool,
|
2017-05-14 19:49:12 +02:00
|
|
|
max_depth: usize,
|
|
|
|
extension_styles: Option<ExtensionStyles>
|
2017-05-12 15:44:09 +02:00
|
|
|
}
|
|
|
|
|
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-14 17:53:14 +02:00
|
|
|
fn print_entry(path_root: &Path, path_entry: &Path, config: &FdOptions) {
|
|
|
|
let path_full = path_root.join(path_entry);
|
|
|
|
|
|
|
|
let path_str = match path_entry.to_str() {
|
2017-05-12 23:23:57 +02:00
|
|
|
Some(p) => p,
|
|
|
|
None => return
|
|
|
|
};
|
|
|
|
|
2017-05-14 17:53:14 +02:00
|
|
|
if !config.colored {
|
2017-05-12 15:44:09 +02:00
|
|
|
println!("{}", path_str);
|
2017-05-14 17:53:14 +02:00
|
|
|
} else {
|
|
|
|
let mut component_path = path_root.to_path_buf();
|
|
|
|
|
|
|
|
// Traverse the path and colorize each component
|
|
|
|
for component in path_entry.components() {
|
|
|
|
let comp_str = match component {
|
|
|
|
Component::Normal(p) => p,
|
|
|
|
_ => error("Unexpected path component")
|
|
|
|
};
|
|
|
|
|
|
|
|
component_path.push(Path::new(comp_str));
|
|
|
|
|
|
|
|
let style =
|
|
|
|
if component_path.symlink_metadata()
|
|
|
|
.map(|md| md.file_type().is_symlink())
|
|
|
|
.unwrap_or(false) {
|
2017-05-14 19:49:12 +02:00
|
|
|
Colour::Cyan.normal()
|
2017-05-14 17:53:14 +02:00
|
|
|
} else if component_path.is_dir() {
|
2017-05-14 19:49:12 +02:00
|
|
|
Colour::Blue.bold()
|
2017-05-14 17:53:14 +02:00
|
|
|
} else {
|
2017-05-14 19:49:12 +02:00
|
|
|
// Loop up file extension
|
|
|
|
if let Some(ref ext_styles) = config.extension_styles {
|
|
|
|
component_path.extension()
|
|
|
|
.and_then(|e| e.to_str())
|
|
|
|
.and_then(|e| ext_styles.get(e))
|
|
|
|
.map(|r| r.clone())
|
2017-05-14 20:49:08 +02:00
|
|
|
.unwrap_or(Style::new())
|
2017-05-14 19:49:12 +02:00
|
|
|
}
|
|
|
|
else {
|
2017-05-14 20:49:08 +02:00
|
|
|
Style::new()
|
2017-05-14 19:49:12 +02:00
|
|
|
}
|
2017-05-14 17:53:14 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
print!("{}", style.paint(comp_str.to_str().unwrap()));
|
|
|
|
|
|
|
|
if component_path.is_dir() && component_path != path_full {
|
|
|
|
let sep = std::path::MAIN_SEPARATOR.to_string();
|
|
|
|
print!("{}", style.paint(sep));
|
|
|
|
}
|
|
|
|
}
|
2017-05-14 19:49:12 +02:00
|
|
|
println!();
|
2017-05-12 15:44:09 +02:00
|
|
|
}
|
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 22:44:06 +02:00
|
|
|
.max_depth(config.max_depth)
|
2017-05-12 19:34:31 +02:00
|
|
|
.into_iter()
|
2017-05-14 22:32:50 +02:00
|
|
|
.filter_entry(|e| config.search_hidden
|
|
|
|
|| !is_hidden(e)
|
|
|
|
|| e.path() == root)
|
2017-05-12 19:34:31 +02:00
|
|
|
.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
|
|
|
|
};
|
|
|
|
|
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 22:29:44 +02:00
|
|
|
|
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))
|
2017-05-14 17:53:14 +02:00
|
|
|
.map(|_| print_entry(&root, 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`.
|
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);
|
|
|
|
}
|
|
|
|
|
2017-05-14 19:49:12 +02:00
|
|
|
/// Parse `dircolors` file.
|
|
|
|
fn parse_dircolors(path: &Path) -> std::io::Result<ExtensionStyles> {
|
|
|
|
let file = File::open(path)?;
|
|
|
|
let mut ext_styles = HashMap::new();
|
|
|
|
|
2017-05-14 20:49:08 +02:00
|
|
|
let pattern_ansi_256 =
|
|
|
|
Regex::new(r"^\.([A-Za-z0-9]+)\s*(?:00;)?38;5;([0-9]+)\b")
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let pattern_ansi =
|
2017-05-14 22:06:15 +02:00
|
|
|
Regex::new(r"^\.([A-Za-z0-9]+)\s*(?:([0-9]+);)?([0-9][0-9])\b")
|
2017-05-14 20:49:08 +02:00
|
|
|
.unwrap();
|
2017-05-14 19:49:12 +02:00
|
|
|
|
|
|
|
for line in BufReader::new(file).lines() {
|
2017-05-14 20:49:08 +02:00
|
|
|
let line_s = line.unwrap();
|
|
|
|
if let Some(caps) = pattern_ansi_256.captures(line_s.as_str()) {
|
2017-05-14 19:49:12 +02:00
|
|
|
if let Some(ext) = caps.get(1).map(|m| m.as_str()) {
|
|
|
|
let fg = caps.get(2)
|
|
|
|
.map(|m| m.as_str())
|
|
|
|
.and_then(|n| u8::from_str_radix(n, 10).ok())
|
|
|
|
.unwrap_or(7); // white
|
|
|
|
ext_styles.insert(String::from(ext),
|
|
|
|
Colour::Fixed(fg).normal());
|
|
|
|
}
|
2017-05-14 20:49:08 +02:00
|
|
|
} else if let Some(caps) = pattern_ansi.captures(line_s.as_str()) {
|
|
|
|
if let Some(ext) = caps.get(1).map(|m| m.as_str()) {
|
|
|
|
let color_s = caps.get(3)
|
|
|
|
.map_or("", |m| m.as_str());
|
|
|
|
let color = match color_s {
|
|
|
|
"31" => Colour::Red,
|
|
|
|
"32" => Colour::Green,
|
|
|
|
"33" => Colour::Yellow,
|
|
|
|
"34" => Colour::Blue,
|
|
|
|
"35" => Colour::Purple,
|
|
|
|
"36" => Colour::Cyan,
|
|
|
|
_ => Colour::White
|
|
|
|
};
|
|
|
|
let style_s = caps.get(2)
|
|
|
|
.map_or("", |m| m.as_str());
|
|
|
|
let style = match style_s {
|
2017-05-14 22:06:15 +02:00
|
|
|
"1" => color.bold(),
|
2017-05-14 20:49:08 +02:00
|
|
|
"01" => color.bold(),
|
2017-05-14 22:06:15 +02:00
|
|
|
"3" => color.italic(),
|
2017-05-14 20:49:08 +02:00
|
|
|
"03" => color.italic(),
|
2017-05-14 22:06:15 +02:00
|
|
|
"4" => color.underline(),
|
2017-05-14 20:49:08 +02:00
|
|
|
"04" => color.underline(),
|
|
|
|
_ => color.normal()
|
|
|
|
};
|
|
|
|
ext_styles.insert(String::from(ext), style);
|
|
|
|
}
|
2017-05-14 19:49:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(ext_styles)
|
|
|
|
}
|
|
|
|
|
2017-05-12 11:50:03 +02:00
|
|
|
fn main() {
|
|
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
|
|
|
|
let mut opts = Options::new();
|
2017-05-15 21:48:14 +02:00
|
|
|
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)");
|
2017-05-15 21:44:47 +02:00
|
|
|
opts.optflag("p", "full-path",
|
|
|
|
"search full path (default: filename only)");
|
2017-05-14 15:46:37 +02:00
|
|
|
opts.optflag("H", "hidden",
|
2017-05-12 22:29:44 +02:00
|
|
|
"search hidden files/directories (default: off)");
|
2017-05-15 21:48:14 +02:00
|
|
|
opts.optflag("f", "follow",
|
|
|
|
"follow symlinks (default: off)");
|
|
|
|
opts.optflag("n", "no-color",
|
|
|
|
"do not colorize output (default: on)");
|
2017-05-15 21:44:47 +02:00
|
|
|
opts.optopt( "d", "max-depth",
|
|
|
|
format!("maximum search depth (default: {})",
|
2017-05-14 17:19:45 +02:00
|
|
|
MAX_DEPTH_DEFAULT).as_str(),
|
2017-05-15 21:48:14 +02:00
|
|
|
"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,
|
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") {
|
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);
|
|
|
|
|
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-15 21:41:31 +02:00
|
|
|
let colored_output = !matches.opt_present("no-color") &&
|
|
|
|
stdout_isatty();
|
|
|
|
|
|
|
|
let ext_styles =
|
|
|
|
if colored_output {
|
|
|
|
env::home_dir()
|
|
|
|
.map(|h| h.join(".dir_colors"))
|
|
|
|
.and_then(|path| parse_dircolors(&path).ok())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
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 22:29:44 +02:00
|
|
|
case_sensitive: matches.opt_present("sensitive") ||
|
2017-05-12 15:44:09 +02:00
|
|
|
pattern.chars().any(char::is_uppercase),
|
2017-05-15 21:44:47 +02:00
|
|
|
search_full_path: matches.opt_present("full-path"),
|
2017-05-12 22:29:44 +02:00
|
|
|
search_hidden: matches.opt_present("hidden"),
|
2017-05-15 21:41:31 +02:00
|
|
|
colored: colored_output,
|
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-14 19:49:12 +02:00
|
|
|
.unwrap_or(MAX_DEPTH_DEFAULT),
|
|
|
|
extension_styles: ext_styles
|
2017-05-12 15:44:09 +02:00
|
|
|
};
|
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
|
|
|
}
|
|
|
|
}
|