2017-06-11 14:15:25 +02:00
|
|
|
#[macro_use]
|
|
|
|
extern crate clap;
|
2017-05-12 13:02:20 +02:00
|
|
|
extern crate ansi_term;
|
2017-06-10 17:30:48 +02:00
|
|
|
extern crate atty;
|
2017-05-12 22:50:52 +02:00
|
|
|
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;
|
2017-06-05 16:25:13 +02:00
|
|
|
pub mod fshelper;
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-06-11 15:28:18 +02:00
|
|
|
use std::borrow::Cow;
|
2017-05-12 11:50:03 +02:00
|
|
|
use std::env;
|
2017-05-12 19:34:31 +02:00
|
|
|
use std::error::Error;
|
2017-06-01 22:46:15 +02:00
|
|
|
use std::io::Write;
|
2017-06-11 15:28:18 +02:00
|
|
|
use std::ops::Deref;
|
2017-06-09 12:18:12 +02:00
|
|
|
#[cfg(target_family = "unix")]
|
2017-06-05 21:50:46 +02:00
|
|
|
use std::os::unix::fs::PermissionsExt;
|
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-06-11 14:15:25 +02:00
|
|
|
use clap::{App, AppSettings, Arg};
|
2017-06-10 17:30:48 +02:00
|
|
|
use atty::Stream;
|
2017-05-12 22:50:52 +02:00
|
|
|
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
|
|
|
|
2017-06-05 21:18:27 +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*.
|
2017-05-12 15:44:09 +02:00
|
|
|
struct FdOptions {
|
2017-06-05 14:14:01 +02:00
|
|
|
/// Determines whether the regex search is case-sensitive or case-insensitive.
|
2017-05-12 15:44:09 +02:00
|
|
|
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).
|
2017-05-12 15:44:09 +02:00
|
|
|
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.
|
2017-05-12 15:44:09 +02:00
|
|
|
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
|
|
|
|
2017-06-05 21:18:27 +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>
|
2017-05-12 15:44:09 +02:00
|
|
|
}
|
|
|
|
|
2017-06-05 21:18:27 +02:00
|
|
|
/// Root directory
|
2017-06-06 20:36:17 +02:00
|
|
|
static ROOT_DIR : &'static str = "/";
|
2017-06-05 21:18:27 +02:00
|
|
|
|
2017-06-11 15:28:18 +02:00
|
|
|
/// Parent directory
|
|
|
|
static PARENT_DIR : &'static str = "..";
|
|
|
|
|
2017-05-12 13:02:20 +02:00
|
|
|
/// Print a search result to the console.
|
2017-06-05 21:18:27 +02:00
|
|
|
fn print_entry(base: &Path, entry: &Path, config: &FdOptions) {
|
|
|
|
let path_full = base.join(entry);
|
2017-05-14 17:53:14 +02:00
|
|
|
|
2017-06-11 15:28:18 +02:00
|
|
|
let path_str = entry.to_string_lossy();
|
2017-05-12 23:23:57 +02:00
|
|
|
|
2017-06-09 12:18:12 +02:00
|
|
|
#[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)
|
|
|
|
};
|
|
|
|
|
2017-06-09 12:18:12 +02:00
|
|
|
#[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();
|
|
|
|
|
2017-06-05 21:18:27 +02:00
|
|
|
let mut component_path = base.to_path_buf();
|
|
|
|
|
|
|
|
if config.path_display == PathDisplay::Absolute {
|
|
|
|
print!("{}", ls_colors.directory.paint(ROOT_DIR));
|
|
|
|
}
|
2017-05-14 17:53:14 +02:00
|
|
|
|
|
|
|
// Traverse the path and colorize each component
|
2017-06-05 21:18:27 +02:00
|
|
|
for component in entry.components() {
|
2017-05-14 17:53:14 +02:00
|
|
|
let comp_str = match component {
|
2017-06-11 15:28:18 +02:00
|
|
|
Component::Normal(p) => p.to_string_lossy(),
|
|
|
|
Component::ParentDir => Cow::from(PARENT_DIR),
|
2017-06-11 20:55:01 +02:00
|
|
|
_ => error("Error: unexpected path component.")
|
2017-05-14 17:53:14 +02:00
|
|
|
};
|
|
|
|
|
2017-06-11 15:28:18 +02:00
|
|
|
component_path.push(Path::new(comp_str.deref()));
|
2017-05-14 17:53:14 +02:00
|
|
|
|
|
|
|
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
|
2017-05-14 17:53:14 +02:00
|
|
|
} 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
|
2017-05-14 17:53:14 +02:00
|
|
|
} 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
|
|
|
}
|
2017-05-14 17:53:14 +02:00
|
|
|
};
|
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
print!("{}", style.paint(comp_str));
|
2017-05-14 17:53:14 +02:00
|
|
|
|
|
|
|
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 {
|
2017-06-07 23:58:08 +02:00
|
|
|
// Uncolorized output
|
2017-06-05 21:18:27 +02:00
|
|
|
|
2017-06-07 23:58:08 +02:00
|
|
|
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-06-05 21:18:27 +02:00
|
|
|
}
|
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-06-05 21:18:27 +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)
|
2017-05-12 15:44:09 +02:00
|
|
|
.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 15:44:09 +02:00
|
|
|
|
2017-05-12 19:34:31 +02:00
|
|
|
for entry in walker {
|
2017-06-05 21:18:27 +02:00
|
|
|
let path_rel_buf = match fshelper::path_relative_from(entry.path(), base) {
|
2017-06-05 16:25:13 +02:00
|
|
|
Some(p) => p,
|
2017-06-11 20:41:32 +02:00
|
|
|
None => error("Error: could not get relative path for directory entry.")
|
2017-05-12 22:20:14 +02:00
|
|
|
};
|
2017-06-05 16:25:13 +02:00
|
|
|
let path_rel = path_rel_buf.as_path();
|
2017-05-12 22:20:14 +02:00
|
|
|
|
2017-06-11 15:28:18 +02:00
|
|
|
let search_str_o =
|
2017-05-12 23:23:57 +02:00
|
|
|
if config.search_full_path {
|
2017-06-11 15:28:18 +02:00
|
|
|
Some(path_rel.to_string_lossy())
|
2017-05-12 23:23:57 +02:00
|
|
|
} else {
|
|
|
|
path_rel.file_name()
|
2017-06-11 15:28:18 +02:00
|
|
|
.map(|f| f.to_string_lossy())
|
2017-05-12 23:23:57 +02:00
|
|
|
};
|
2017-05-12 22:20:14 +02:00
|
|
|
|
2017-06-11 15:28:18 +02:00
|
|
|
if let Some(search_str) = search_str_o {
|
|
|
|
pattern.find(&*search_str)
|
|
|
|
.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`.
|
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() {
|
2017-06-11 14:15:25 +02:00
|
|
|
let matches =
|
|
|
|
App::new("fd")
|
|
|
|
.version(crate_version!())
|
|
|
|
.usage("fd [FLAGS/OPTIONS] [<pattern>] [<path>]")
|
|
|
|
.setting(AppSettings::ColoredHelp)
|
|
|
|
.setting(AppSettings::DeriveDisplayOrder)
|
|
|
|
.arg(Arg::with_name("case-sensitive")
|
|
|
|
.long("case-sensitive")
|
|
|
|
.short("s")
|
|
|
|
.help("Case-sensitive search (default: smart case)"))
|
|
|
|
.arg(Arg::with_name("full-path")
|
|
|
|
.long("full-path")
|
|
|
|
.short("p")
|
|
|
|
.help("Search full path (default: file-/dirname only)"))
|
|
|
|
.arg(Arg::with_name("hidden")
|
|
|
|
.long("hidden")
|
|
|
|
.short("H")
|
|
|
|
.help("Search hidden files and directories"))
|
|
|
|
.arg(Arg::with_name("no-ignore")
|
|
|
|
.long("no-ignore")
|
|
|
|
.short("I")
|
|
|
|
.help("Do not respect .(git)ignore files"))
|
|
|
|
.arg(Arg::with_name("follow")
|
|
|
|
.long("follow")
|
|
|
|
.short("f")
|
|
|
|
.help("Follow symlinks"))
|
|
|
|
.arg(Arg::with_name("absolute-path")
|
|
|
|
.long("absolute-path")
|
|
|
|
.short("a")
|
|
|
|
.help("Show absolute instead of relative paths"))
|
|
|
|
.arg(Arg::with_name("no-color")
|
|
|
|
.long("no-color")
|
|
|
|
.short("n")
|
|
|
|
.help("Do not colorize output"))
|
|
|
|
.arg(Arg::with_name("depth")
|
|
|
|
.long("max-depth")
|
|
|
|
.short("d")
|
|
|
|
.takes_value(true)
|
|
|
|
.help("Set maximum search depth (default: none)"))
|
|
|
|
.arg(Arg::with_name("pattern")
|
|
|
|
.help("the search pattern, a regular expression (optional)"))
|
|
|
|
.arg(Arg::with_name("path")
|
|
|
|
.help("the root directory for the filesystem search (optional)"))
|
|
|
|
.get_matches();
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
// Get the search pattern
|
|
|
|
let empty_pattern = String::new();
|
2017-06-11 14:15:25 +02:00
|
|
|
let pattern = matches.value_of("pattern").unwrap_or(&empty_pattern);
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
// Get the current working directory
|
2017-05-12 12:39:13 +02:00
|
|
|
let current_dir_buf = match env::current_dir() {
|
|
|
|
Ok(cd) => cd,
|
2017-06-11 20:55:01 +02:00
|
|
|
Err(_) => error("Error: could not get current directory.")
|
2017-05-12 12:39:13 +02:00
|
|
|
};
|
2017-05-12 11:50:03 +02:00
|
|
|
let current_dir = current_dir_buf.as_path();
|
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
// Get the root directory for the search
|
2017-06-11 20:55:01 +02:00
|
|
|
let mut root_dir_is_absolute = false;
|
2017-06-11 20:41:32 +02:00
|
|
|
let root_dir_buf = if let Some(rd) = matches.value_of("path") {
|
2017-06-11 20:55:01 +02:00
|
|
|
let path = Path::new(rd);
|
|
|
|
|
|
|
|
root_dir_is_absolute = path.is_absolute();
|
|
|
|
|
|
|
|
path.canonicalize().unwrap_or_else(
|
|
|
|
|_| error(&format!("Error: could not find directory '{}'.", rd))
|
2017-06-11 20:41:32 +02:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
current_dir_buf.clone()
|
|
|
|
};
|
|
|
|
|
|
|
|
if !root_dir_buf.is_dir() {
|
2017-06-11 20:55:01 +02:00
|
|
|
error(&format!("Error: '{}' is not a directory.", root_dir_buf.to_string_lossy()));
|
2017-06-11 20:41:32 +02:00
|
|
|
}
|
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
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).
|
2017-06-11 14:15:25 +02:00
|
|
|
let case_sensitive = matches.is_present("case-sensitive") ||
|
2017-06-05 14:14:01 +02:00
|
|
|
pattern.chars().any(char::is_uppercase);
|
|
|
|
|
2017-06-11 14:15:25 +02:00
|
|
|
let colored_output = !matches.is_present("no-color") &&
|
2017-06-10 17:30:48 +02:00
|
|
|
atty::is(Stream::Stdout);
|
2017-05-15 21:41:31 +02:00
|
|
|
|
2017-06-01 22:46:15 +02:00
|
|
|
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
|
|
|
|
};
|
2017-05-12 15:44:09 +02:00
|
|
|
|
|
|
|
let config = FdOptions {
|
2017-06-05 14:14:01 +02:00
|
|
|
case_sensitive: case_sensitive,
|
2017-06-11 14:15:25 +02:00
|
|
|
search_full_path: matches.is_present("full-path"),
|
|
|
|
ignore_hidden: !matches.is_present("hidden"),
|
|
|
|
read_ignore: !matches.is_present("no-ignore"),
|
|
|
|
follow_links: matches.is_present("follow"),
|
|
|
|
max_depth: matches.value_of("depth")
|
2017-06-11 20:41:32 +02:00
|
|
|
.and_then(|ds| usize::from_str_radix(ds, 10).ok()),
|
2017-06-11 20:55:01 +02:00
|
|
|
path_display: if matches.is_present("absolute-path") || root_dir_is_absolute {
|
2017-06-05 21:18:27 +02:00
|
|
|
PathDisplay::Absolute
|
|
|
|
} else {
|
|
|
|
PathDisplay::Relative
|
|
|
|
},
|
2017-06-01 22:46:15 +02:00
|
|
|
ls_colors: ls_colors
|
2017-05-12 15:44:09 +02:00
|
|
|
};
|
2017-05-12 13:32:30 +02:00
|
|
|
|
2017-06-05 21:18:27 +02:00
|
|
|
let root = Path::new(ROOT_DIR);
|
|
|
|
let base = match config.path_display {
|
|
|
|
PathDisplay::Relative => current_dir,
|
|
|
|
PathDisplay::Absolute => root
|
|
|
|
};
|
|
|
|
|
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-06-05 21:18:27 +02:00
|
|
|
Ok(re) => scan(root_dir, &re, base, &config),
|
2017-05-12 12:39:13 +02:00
|
|
|
Err(err) => error(err.description())
|
2017-05-12 11:50:03 +02:00
|
|
|
}
|
|
|
|
}
|