fd/src/main.rs

303 lines
10 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;
2017-05-15 22:38:34 +02:00
extern crate ignore;
2017-06-05 11:56:39 +02:00
pub mod lscolors;
pub mod fshelper;
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;
use std::fs;
use std::io::Write;
#[cfg(target_family = "unix")]
2017-06-05 21:50:46 +02:00
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, Component};
2017-05-12 11:50:03 +02:00
use std::process;
use getopts::Options;
use isatty::stdout_isatty;
use regex::{Regex, RegexBuilder};
2017-05-15 22:38:34 +02:00
use ignore::WalkBuilder;
2017-05-12 13:02:20 +02:00
2017-06-05 11:56:39 +02:00
use lscolors::LsColors;
2017-05-14 19:49:12 +02:00
/// Defines how to display search result paths.
#[derive(PartialEq)]
enum PathDisplay {
/// As an absolute path
Absolute,
/// As a relative path
Relative
}
2017-05-14 17:19:45 +02:00
/// Configuration options for *fd*.
struct FdOptions {
2017-06-05 14:14:01 +02:00
/// Determines whether the regex search is case-sensitive or case-insensitive.
case_sensitive: bool,
2017-06-05 14:14:01 +02:00
/// Whether to search within the full file path or just the base name (filename or directory
/// name).
search_full_path: bool,
2017-06-05 14:14:01 +02:00
/// Whether to ignore hidden files and directories (or not).
2017-05-15 22:38:34 +02:00
ignore_hidden: bool,
2017-06-05 14:14:01 +02:00
/// Whether to respect VCS ignore files (`.gitignore`, `.ignore`, ..) or not.
2017-05-15 22:38:34 +02:00
read_ignore: bool,
2017-06-05 14:14:01 +02:00
/// Whether to follow symlinks or not.
follow_links: bool,
2017-06-05 14:14:01 +02:00
/// The maximum search depth, or `None` if no maximum search depth should be set.
///
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
/// all files under subdirectories of the current directory, etc.
2017-05-15 22:38:34 +02:00
max_depth: Option<usize>,
2017-06-05 14:14:01 +02:00
/// Display results as relative or absolute path.
path_display: PathDisplay,
2017-06-05 14:14:01 +02:00
/// `None` if the output should not be colorized. Otherwise, a `LsColors` instance that defines
/// how to style different filetypes.
ls_colors: Option<LsColors>
}
/// Root directory
2017-06-06 20:36:17 +02:00
static ROOT_DIR : &'static str = "/";
2017-05-12 13:02:20 +02:00
/// Print a search result to the console.
fn print_entry(base: &Path, entry: &Path, config: &FdOptions) {
let path_full = base.join(entry);
let path_str = match entry.to_str() {
2017-05-12 23:23:57 +02:00
Some(p) => p,
None => return
};
#[cfg(target_family = "unix")]
2017-06-05 21:50:46 +02:00
let is_executable = |p: &std::path::PathBuf| {
p.metadata()
.ok()
.map(|f| f.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
};
#[cfg(not(target_family = "unix"))]
let is_executable = |p: &std::path::PathBuf| {false};
2017-06-05 14:14:01 +02:00
if let Some(ref ls_colors) = config.ls_colors {
2017-06-09 14:39:57 +02:00
let default_style = ansi_term::Style::default();
let mut component_path = base.to_path_buf();
if config.path_display == PathDisplay::Absolute {
print!("{}", ls_colors.directory.paint(ROOT_DIR));
}
// Traverse the path and colorize each component
for component in entry.components() {
let comp_str = match component {
Component::Normal(p) => p.to_str().unwrap(),
Component::ParentDir => "..",
_ => 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-06-09 14:39:57 +02:00
&ls_colors.symlink
} else if component_path.is_dir() {
2017-06-09 14:39:57 +02:00
&ls_colors.directory
2017-06-05 21:50:46 +02:00
} else if is_executable(&component_path) {
2017-06-09 14:39:57 +02:00
&ls_colors.executable
} else {
2017-06-01 23:08:02 +02:00
// Look up file name
let o_style =
component_path.file_name()
.and_then(|n| n.to_str())
2017-06-05 14:14:01 +02:00
.and_then(|n| ls_colors.filenames.get(n));
2017-06-01 23:08:02 +02:00
match o_style {
2017-06-09 14:39:57 +02:00
Some(s) => s,
2017-06-01 23:08:02 +02:00
None =>
// Look up file extension
component_path.extension()
.and_then(|e| e.to_str())
2017-06-05 14:14:01 +02:00
.and_then(|e| ls_colors.extensions.get(e))
2017-06-09 14:39:57 +02:00
.unwrap_or(&default_style)
2017-06-01 23:08:02 +02:00
}
};
print!("{}", style.paint(comp_str));
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-06-05 14:14:01 +02:00
} else {
// Uncolorized output
let prefix = if config.path_display == PathDisplay::Absolute { ROOT_DIR } else { "" };
let r = writeln!(&mut std::io::stdout(), "{}{}", prefix, path_str);
if r.is_err() {
// Probably a broken pipe. Exit gracefully.
process::exit(0);
}
}
2017-05-12 13:02:20 +02:00
}
2017-05-12 11:50:03 +02:00
/// Recursively scan the given search path and search for files / pathnames matching the pattern.
fn scan(root: &Path, pattern: &Regex, base: &Path, config: &FdOptions) {
2017-05-15 22:38:34 +02:00
let walker = WalkBuilder::new(root)
.hidden(config.ignore_hidden)
.ignore(config.read_ignore)
.git_ignore(config.read_ignore)
.parents(config.read_ignore)
.git_global(config.read_ignore)
.git_exclude(config.read_ignore)
.follow_links(config.follow_links)
2017-05-12 22:44:06 +02:00
.max_depth(config.max_depth)
2017-05-15 22:38:34 +02:00
.build()
2017-05-12 19:34:31 +02:00
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path() != root);
2017-05-12 19:34:31 +02:00
for entry in walker {
let path_rel_buf = match fshelper::path_relative_from(entry.path(), base) {
Some(p) => p,
None => error("Could not get relative path for directory entry.")
2017-05-12 22:20:14 +02:00
};
let path_rel = path_rel_buf.as_path();
2017-05-12 22:20:14 +02:00
2017-05-12 23:23:57 +02:00
let search_str =
if config.search_full_path {
path_rel.to_str()
} else {
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(base, 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();
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)");
opts.optflag("p", "full-path",
"search full path (default: file-/dirname only)");
opts.optflag("H", "hidden",
2017-05-15 22:38:34 +02:00
"search hidden files/directories");
opts.optflag("I", "no-ignore",
"do not respect .(git)ignore files");
2017-05-15 21:48:14 +02:00
opts.optflag("f", "follow",
2017-05-15 22:38:34 +02:00
"follow symlinks");
2017-05-15 21:48:14 +02:00
opts.optflag("n", "no-color",
2017-05-15 22:38:34 +02:00
"do not colorize output");
opts.optopt("d", "max-depth",
"maximum search depth (default: none)", "D");
opts.optflag("a", "absolute",
"show absolute instead of relative paths");
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") {
let brief = "Usage: fd [options] [PATTERN] [PATH]";
2017-06-03 23:28:32 +02:00
print!("{}", opts.usage(brief));
2017-05-12 11:50:03 +02:00
process::exit(1);
}
// Get the search pattern
let empty_pattern = String::new();
let pattern = matches.free.get(0).unwrap_or(&empty_pattern);
2017-05-12 11:50:03 +02:00
// Get the current working directory
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();
// Get the root directory for the search
let root_dir_buf = matches.free.get(1)
.and_then(|r| fs::canonicalize(r).ok())
2017-06-05 18:12:35 +02:00
.unwrap_or_else(|| current_dir_buf.clone());
let root_dir = root_dir_buf.as_path();
2017-06-05 14:14:01 +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).
let case_sensitive = matches.opt_present("sensitive") ||
pattern.chars().any(char::is_uppercase);
let colored_output = !matches.opt_present("no-color") &&
stdout_isatty();
let ls_colors =
2017-06-05 14:14:01 +02:00
if colored_output {
Some(
env::var("LS_COLORS")
.ok()
.map(|val| LsColors::from_string(&val))
.unwrap_or_default()
)
} else {
None
};
let config = FdOptions {
2017-06-05 14:14:01 +02:00
case_sensitive: case_sensitive,
search_full_path: matches.opt_present("full-path"),
2017-05-15 22:38:34 +02:00
ignore_hidden: !matches.opt_present("hidden"),
read_ignore: !matches.opt_present("no-ignore"),
2017-05-12 22:44:06 +02:00
follow_links: matches.opt_present("follow"),
2017-06-05 14:14:01 +02:00
max_depth: matches.opt_str("max-depth")
.and_then(|ds| usize::from_str_radix(&ds, 10).ok()),
path_display: if matches.opt_present("absolute") {
PathDisplay::Absolute
} else {
PathDisplay::Relative
},
ls_colors: ls_colors
};
2017-05-12 13:32:30 +02:00
let root = Path::new(ROOT_DIR);
let base = match config.path_display {
PathDisplay::Relative => current_dir,
PathDisplay::Absolute => root
};
match RegexBuilder::new(pattern)
.case_insensitive(!config.case_sensitive)
.build() {
Ok(re) => scan(root_dir, &re, base, &config),
Err(err) => error(err.description())
2017-05-12 11:50:03 +02:00
}
}