fd/src/main.rs

432 lines
15 KiB
Rust
Raw Normal View History

2017-10-04 14:31:08 +02:00
mod app;
2020-04-03 21:24:11 +02:00
mod error;
2017-10-14 18:04:11 +02:00
mod exec;
mod exit_codes;
2020-04-03 12:04:47 +02:00
mod filesystem;
2020-04-03 11:34:34 +02:00
mod filetypes;
2020-04-03 11:36:54 +02:00
mod filter;
2020-04-03 11:39:32 +02:00
mod options;
2017-10-10 08:01:17 +02:00
mod output;
2020-04-03 11:44:45 +02:00
mod regex_helper;
2017-10-10 08:01:17 +02:00
mod walk;
2017-05-12 11:50:03 +02:00
use std::env;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Arc;
use std::time;
2017-05-12 11:50:03 +02:00
2020-04-03 20:55:14 +02:00
use anyhow::{anyhow, Context, Result};
2017-06-10 17:30:48 +02:00
use atty::Stream;
use globset::GlobBuilder;
use lscolors::LsColors;
use regex::bytes::{RegexBuilder, RegexSetBuilder};
2017-05-12 13:02:20 +02:00
2020-05-13 13:26:47 +02:00
use crate::error::print_error;
use crate::exec::CommandTemplate;
2020-04-03 20:55:14 +02:00
use crate::exit_codes::ExitCode;
2020-04-03 11:34:34 +02:00
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
2020-04-03 11:36:54 +02:00
use crate::filter::{SizeFilter, TimeFilter};
2020-04-03 11:39:32 +02:00
use crate::options::Options;
2020-04-03 11:44:45 +02:00
use crate::regex_helper::pattern_has_uppercase_char;
2017-05-12 11:50:03 +02:00
2019-09-15 18:13:29 +02:00
// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
// FIXME: re-enable jemalloc on macOS, see comment in Cargo.toml file for more infos
#[cfg(all(not(windows), not(target_os = "macos"), not(target_env = "musl")))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
2020-04-03 20:55:14 +02:00
fn run() -> Result<ExitCode> {
let matches = app::build_app().get_matches_from(env::args_os());
2017-05-12 11:50:03 +02:00
// Set the current working directory of the process
if let Some(base_directory) = matches.value_of_os("base-directory") {
2020-04-03 19:41:43 +02:00
let base_directory = Path::new(base_directory);
if !filesystem::is_dir(base_directory) {
2020-04-03 20:55:14 +02:00
return Err(anyhow!(
"The '--base-directory' path '{}' is not a directory.",
2020-04-03 19:41:43 +02:00
base_directory.to_string_lossy()
2020-04-03 20:55:14 +02:00
));
}
2020-04-03 20:55:14 +02:00
env::set_current_dir(base_directory).with_context(|| {
format!(
"Could not set '{}' as the current working directory.",
base_directory.to_string_lossy()
)
})?;
}
2020-04-03 19:41:43 +02:00
let current_directory = Path::new(".");
if !filesystem::is_dir(current_directory) {
2020-04-03 20:55:14 +02:00
return Err(anyhow!(
"Could not retrieve current directory (has it been deleted?)."
));
}
2017-05-12 11:50:03 +02:00
2020-04-03 19:41:43 +02:00
// Get the search pattern
let pattern = matches
.value_of_os("pattern")
.map(|p| {
2020-05-01 11:22:30 +02:00
p.to_str()
.ok_or_else(|| anyhow!("The search pattern includes invalid UTF-8 sequences."))
})
.transpose()?
.unwrap_or("");
2020-04-03 19:41:43 +02:00
2018-03-14 22:49:53 +01:00
// Get one or more root directories to search.
2020-05-19 13:40:19 +02:00
let passed_arguments = matches
.values_of_os("path")
2020-05-19 13:40:19 +02:00
.or_else(|| matches.values_of_os("search-path"));
2020-05-19 15:37:17 +02:00
let mut search_paths = if let Some(paths) = passed_arguments {
let mut directories = vec![];
2020-05-19 13:40:19 +02:00
for path in paths {
let path_buffer = PathBuf::from(path);
if filesystem::is_dir(&path_buffer) {
2020-05-19 15:37:17 +02:00
directories.push(path_buffer);
} else {
2020-05-19 14:31:56 +02:00
print_error(format!(
"Search path '{}' is not a directory.",
path_buffer.to_string_lossy()
));
2020-05-19 13:40:19 +02:00
}
}
2020-05-19 15:37:17 +02:00
directories
} else {
vec![current_directory.to_path_buf()]
};
// Check if we have no valid search paths.
2020-05-19 15:37:17 +02:00
if search_paths.is_empty() {
2020-05-19 14:31:56 +02:00
return Err(anyhow!("No valid search paths given."));
}
Add multiple path support (#182) * Adding support for multiple paths. (panic) - Started adding multiple file support - fd panics with multiple files right now * Moved the ctrlc handler to main. - Moved the ctrlc handler to main so we can search multiple files * Tests now allow custom directory setup - TestEnv::new() now takes two arguments, the directories to create and the files to create inside those directories. * rust-fmt changes * rust-fmt changes * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Removed commented code in testenv * Refactored walk::scan to accept the path buffer vector. Using the ParallelWalker allows for multithreaded searching of multiple directories * Moved ctrlc handler back into walker, it is only called once from main now. * Moved the colored output check back to it's original place * Removing shell-escape, not sure how it got added... * Added test for `fd 'a.foo' test1` to show that a.foo is only found in the test1 and not the test2 direcotry * Removing side effect from walk::scan, `dir_vec` is no longer a mutable reference and an iterator is being used instead. * Running rustfmt to format code correctly
2017-12-06 23:52:23 +01:00
if matches.is_present("absolute-path") {
2020-05-19 15:37:17 +02:00
search_paths = search_paths
Add multiple path support (#182) * Adding support for multiple paths. (panic) - Started adding multiple file support - fd panics with multiple files right now * Moved the ctrlc handler to main. - Moved the ctrlc handler to main so we can search multiple files * Tests now allow custom directory setup - TestEnv::new() now takes two arguments, the directories to create and the files to create inside those directories. * rust-fmt changes * rust-fmt changes * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Removed commented code in testenv * Refactored walk::scan to accept the path buffer vector. Using the ParallelWalker allows for multithreaded searching of multiple directories * Moved ctrlc handler back into walker, it is only called once from main now. * Moved the colored output check back to it's original place * Removing shell-escape, not sure how it got added... * Added test for `fd 'a.foo' test1` to show that a.foo is only found in the test1 and not the test2 direcotry * Removing side effect from walk::scan, `dir_vec` is no longer a mutable reference and an iterator is being used instead. * Running rustfmt to format code correctly
2017-12-06 23:52:23 +01:00
.iter()
.map(|path_buffer| {
path_buffer
.canonicalize()
2020-04-03 12:04:47 +02:00
.and_then(|pb| filesystem::absolute_path(pb.as_path()))
.unwrap()
2018-09-27 23:01:38 +02:00
})
.collect();
}
// Detect if the user accidentally supplied a path instead of a search pattern
2018-05-14 18:39:47 +02:00
if !matches.is_present("full-path")
&& pattern.contains(std::path::MAIN_SEPARATOR)
2020-04-03 12:04:47 +02:00
&& filesystem::is_dir(Path::new(pattern))
{
2020-04-03 20:55:14 +02:00
return Err(anyhow!(
2018-10-27 16:30:29 +02:00
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
and will not lead to any search results.\n\n\
If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \
fd . '{pattern}'\n\n\
2020-04-03 20:55:14 +02:00
Instead, if you want your pattern to match the full file path, use:\n\n \
fd --full-path '{pattern}'",
pattern = pattern,
sep = std::path::MAIN_SEPARATOR,
2020-04-03 20:55:14 +02:00
));
}
2020-05-01 11:22:30 +02:00
let pattern_regex = if matches.is_present("glob") && !pattern.is_empty() {
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
glob.regex().to_owned()
} else if matches.is_present("fixed-strings") {
// Treat pattern as literal string if '--fixed-strings' is used
2018-02-10 15:19:53 +01:00
regex::escape(pattern)
} else {
String::from(pattern)
};
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).
2018-01-01 12:16:43 +01:00
let case_sensitive = !matches.is_present("ignore-case")
2018-02-10 15:19:53 +01:00
&& (matches.is_present("case-sensitive") || pattern_has_uppercase_char(&pattern_regex));
2017-06-05 14:14:01 +02:00
#[cfg(windows)]
let ansi_colors_support =
ansi_term::enable_ansi_support().is_ok() || std::env::var_os("TERM").is_some();
#[cfg(not(windows))]
let ansi_colors_support = true;
let interactive_terminal = atty::is(Stream::Stdout);
let colored_output = match matches.value_of("color") {
Some("always") => true,
Some("never") => false,
_ => ansi_colors_support && env::var_os("NO_COLOR").is_none() && interactive_terminal,
};
2019-04-12 02:16:02 +02:00
let path_separator = matches.value_of("path-separator").map(|str| str.to_owned());
2017-10-12 08:01:51 +02:00
let ls_colors = if colored_output {
Some(LsColors::from_env().unwrap_or_default())
2017-10-12 08:01:51 +02:00
} else {
None
};
2020-04-03 20:55:14 +02:00
let command = if let Some(args) = matches.values_of("exec") {
Some(CommandTemplate::new(args))
} else if let Some(args) = matches.values_of("exec-batch") {
Some(CommandTemplate::new_batch(args)?)
} else if matches.is_present("list-details") {
let color = matches.value_of("color").unwrap_or("auto");
let color_arg = ["--color=", color].concat();
2020-04-15 23:08:15 +02:00
#[allow(unused)]
let gnu_ls = |command_name| {
vec![
command_name,
"-l", // long listing format
"--human-readable", // human readable file sizes
"--directory", // list directories themselves, not their contents
&color_arg,
]
};
let cmd: Vec<&str> = if cfg!(unix) {
2020-05-01 11:22:30 +02:00
if !cfg!(any(
target_os = "macos",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)) {
2020-04-16 19:44:10 +02:00
// Assume ls is GNU ls
gnu_ls("ls")
} else {
2020-04-16 19:44:10 +02:00
// MacOS, DragonFlyBSD, FreeBSD
2020-04-15 20:31:59 +02:00
use std::process::{Command, Stdio};
2020-04-15 20:34:20 +02:00
// Use GNU ls, if available (support for --color=auto, better LS_COLORS support)
2020-04-15 20:31:59 +02:00
let gnu_ls_exists = Command::new("gls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("gls")
} else {
let mut cmd = vec![
2020-04-16 19:44:10 +02:00
"ls", // BSD version of ls
"-l", // long listing format
2020-04-16 19:44:10 +02:00
"-h", // '--human-readable' is not available, '-h' is
"-d", // '--directory' is not available, but '-d' is
];
2020-04-16 20:10:00 +02:00
if !cfg!(any(target_os = "netbsd", target_os = "openbsd")) && colored_output {
// -G is not available in NetBSD's and OpenBSD's ls
cmd.push("-G");
}
cmd
}
}
} else if cfg!(windows) {
use std::process::{Command, Stdio};
// Use GNU ls, if available
let gnu_ls_exists = Command::new("ls")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok();
if gnu_ls_exists {
gnu_ls("ls")
} else {
return Err(anyhow!(
2020-04-15 23:08:15 +02:00
"'fd --list-details' is not supported on Windows unless GNU 'ls' is installed."
));
}
} else {
return Err(anyhow!(
2020-04-15 23:08:15 +02:00
"'fd --list-details' is not supported on this platform."
));
};
Some(CommandTemplate::new_batch(&cmd).unwrap())
2020-04-03 20:55:14 +02:00
} else {
None
};
2017-10-14 18:04:11 +02:00
2020-04-03 20:55:14 +02:00
let size_limits = if let Some(vs) = matches.values_of("size") {
vs.map(|sf| {
2020-05-01 11:22:30 +02:00
SizeFilter::from_string(sf)
.ok_or_else(|| anyhow!("'{}' is not a valid size constraint. See 'fd --help'.", sf))
2018-09-27 23:01:38 +02:00
})
2020-04-03 20:55:14 +02:00
.collect::<Result<Vec<_>>>()?
} else {
vec![]
};
let now = time::SystemTime::now();
let mut time_constraints: Vec<TimeFilter> = Vec::new();
if let Some(t) = matches.value_of("changed-within") {
if let Some(f) = TimeFilter::after(&now, t) {
time_constraints.push(f);
} else {
2020-04-03 20:55:14 +02:00
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
if let Some(t) = matches.value_of("changed-before") {
if let Some(f) = TimeFilter::before(&now, t) {
time_constraints.push(f);
} else {
2020-04-03 20:55:14 +02:00
return Err(anyhow!(
"'{}' is not a valid date or duration. See 'fd --help'.",
t
));
}
}
#[cfg(unix)]
let owner_constraint = if let Some(s) = matches.value_of("owner") {
OwnerFilter::from_string(s)?
} else {
None
};
2020-04-03 11:39:32 +02:00
let config = Options {
2017-10-14 18:04:11 +02:00
case_sensitive,
2017-10-12 08:01:51 +02:00
search_full_path: matches.is_present("full-path"),
2018-01-01 12:16:43 +01:00
ignore_hidden: !(matches.is_present("hidden")
|| matches.occurrences_of("rg-alias-hidden-ignore") >= 2),
2018-02-21 21:41:52 +01:00
read_fdignore: !(matches.is_present("no-ignore")
2018-01-01 12:16:43 +01:00
|| matches.is_present("rg-alias-hidden-ignore")),
2018-02-21 21:41:52 +01:00
read_vcsignore: !(matches.is_present("no-ignore")
2018-01-01 12:16:43 +01:00
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-ignore-vcs")),
2020-04-25 21:32:17 +02:00
read_global_ignore: !(matches.is_present("no-ignore")
|| matches.is_present("rg-alias-hidden-ignore")
|| matches.is_present("no-global-ignore-file")),
2017-10-12 08:01:51 +02:00
follow_links: matches.is_present("follow"),
one_file_system: matches.is_present("one-file-system"),
2017-10-12 08:01:51 +02:00
null_separator: matches.is_present("null_separator"),
2018-01-01 12:16:43 +01:00
max_depth: matches
.value_of("max-depth")
.or_else(|| matches.value_of("rg-depth"))
.or_else(|| matches.value_of("exact-depth"))
.and_then(|n| usize::from_str_radix(n, 10).ok()),
min_depth: matches
.value_of("min-depth")
.or_else(|| matches.value_of("exact-depth"))
2018-01-01 12:16:43 +01:00
.and_then(|n| usize::from_str_radix(n, 10).ok()),
2017-10-12 08:01:51 +02:00
threads: std::cmp::max(
matches
.value_of("threads")
.and_then(|n| usize::from_str_radix(n, 10).ok())
.unwrap_or_else(num_cpus::get),
1,
),
max_buffer_time: matches
.value_of("max-buffer-time")
.and_then(|n| u64::from_str_radix(n, 10).ok())
.map(time::Duration::from_millis),
2017-10-14 18:04:11 +02:00
ls_colors,
interactive_terminal,
file_types: matches.values_of("file-type").map(|values| {
let mut file_types = FileTypes::default();
for value in values {
match value {
"f" | "file" => file_types.files = true,
"d" | "directory" => file_types.directories = true,
"l" | "symlink" => file_types.symlinks = true,
2018-03-25 12:19:51 +02:00
"x" | "executable" => {
file_types.executables_only = true;
file_types.files = true;
2018-03-25 16:36:37 +02:00
}
"e" | "empty" => file_types.empty_only = true,
"s" | "socket" => file_types.sockets = true,
"p" | "pipe" => file_types.pipes = true,
_ => unreachable!(),
}
}
// If only 'empty' was specified, search for both files and directories:
if file_types.empty_only && !(file_types.files || file_types.directories) {
file_types.files = true;
file_types.directories = true;
}
file_types
}),
2020-04-03 20:55:14 +02:00
extensions: matches
.values_of("extension")
.map(|exts| {
let patterns = exts
.map(|e| e.trim_start_matches('.'))
.map(|e| format!(r".\.{}$", regex::escape(e)));
RegexSetBuilder::new(patterns)
.case_insensitive(true)
.build()
})
.transpose()?,
command: command.map(Arc::new),
exclude_patterns: matches
.values_of("exclude")
.map(|v| v.map(|p| String::from("!") + p).collect())
2017-10-26 21:13:56 +02:00
.unwrap_or_else(|| vec![]),
2018-03-26 00:15:01 +02:00
ignore_files: matches
.values_of("ignore-file")
.map(|vs| vs.map(PathBuf::from).collect())
.unwrap_or_else(|| vec![]),
size_constraints: size_limits,
time_constraints,
#[cfg(unix)]
owner_constraint,
2018-10-22 14:20:08 +02:00
show_filesystem_errors: matches.is_present("show-errors"),
path_separator,
max_results: matches
.value_of("max-results")
.and_then(|n| usize::from_str_radix(n, 10).ok())
2020-04-09 17:21:40 +02:00
.filter(|&n| n != 0)
.or_else(|| {
if matches.is_present("max-one-result") {
Some(1)
} else {
None
}
}),
};
2017-05-12 13:32:30 +02:00
2020-04-03 20:55:14 +02:00
let re = RegexBuilder::new(&pattern_regex)
2017-10-12 08:01:51 +02:00
.case_insensitive(!config.case_sensitive)
.dot_matches_new_line(true)
2018-01-01 12:16:43 +01:00
.build()
2020-04-03 20:55:14 +02:00
.map_err(|e| {
anyhow!(
"{}\n\nNote: You can use the '--fixed-strings' option to search for a \
literal string instead of a regular expression. Alternatively, you can \
also use the '--glob' option to match on a glob pattern.",
e.to_string()
)
})?;
2020-05-19 15:37:17 +02:00
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
2020-04-03 20:55:14 +02:00
}
fn main() {
let result = run();
match result {
Ok(exit_code) => {
process::exit(exit_code.into());
}
Err(err) => {
2020-04-03 20:55:14 +02:00
eprintln!("[fd error]: {}", err);
2020-04-03 21:34:59 +02:00
process::exit(ExitCode::GeneralError.into());
}
2017-05-12 11:50:03 +02:00
}
}