2017-10-21 10:16:03 +02:00
|
|
|
// Copyright (c) 2017 fd developers
|
|
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
|
|
|
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
|
|
|
// at your option. All files in the project carrying such
|
|
|
|
// notice may not be copied, modified, or distributed except
|
|
|
|
// according to those terms.
|
|
|
|
|
2018-10-03 15:53:59 +02:00
|
|
|
#[macro_use]
|
|
|
|
mod internal;
|
|
|
|
|
2017-10-04 14:31:08 +02:00
|
|
|
mod app;
|
2017-10-14 18:04:11 +02:00
|
|
|
mod exec;
|
2018-10-03 11:25:16 +02:00
|
|
|
mod exit_codes;
|
2018-04-13 22:46:17 +02:00
|
|
|
pub mod fshelper;
|
2017-10-10 08:01:17 +02:00
|
|
|
mod output;
|
|
|
|
mod walk;
|
2017-05-12 11:50:03 +02:00
|
|
|
|
|
|
|
use std::env;
|
2017-05-12 19:34:31 +02:00
|
|
|
use std::error::Error;
|
2017-10-14 19:57:15 +02:00
|
|
|
use std::path::{Path, PathBuf};
|
2019-09-13 22:26:27 +02:00
|
|
|
use std::process;
|
2017-06-11 22:43:30 +02:00
|
|
|
use std::sync::Arc;
|
2017-06-16 10:37:40 +02:00
|
|
|
use std::time;
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-06-10 17:30:48 +02:00
|
|
|
use atty::Stream;
|
2019-09-15 15:37:08 +02:00
|
|
|
use globset::Glob;
|
2018-12-09 14:56:05 +01:00
|
|
|
use lscolors::LsColors;
|
2019-09-15 14:42:48 +02:00
|
|
|
use regex::bytes::{RegexBuilder, RegexSetBuilder};
|
2017-05-12 13:02:20 +02:00
|
|
|
|
2019-01-05 18:17:22 +01:00
|
|
|
use crate::exec::CommandTemplate;
|
|
|
|
use crate::internal::{
|
2018-10-12 09:06:14 +02:00
|
|
|
filter::{SizeFilter, TimeFilter},
|
|
|
|
opts::FdOptions,
|
|
|
|
pattern_has_uppercase_char, transform_args_with_exec, FileTypes,
|
2018-05-14 18:39:47 +02:00
|
|
|
};
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2019-09-15 17:47:36 +02:00
|
|
|
// We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/480
|
|
|
|
#[global_allocator]
|
|
|
|
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
|
|
|
|
2017-05-12 11:50:03 +02:00
|
|
|
fn main() {
|
2018-01-29 20:32:46 +01:00
|
|
|
let checked_args = transform_args_with_exec(env::args_os());
|
|
|
|
let matches = app::build_app().get_matches_from(checked_args);
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
// Get the search pattern
|
2017-11-29 04:28:31 +01:00
|
|
|
let pattern = matches.value_of("pattern").unwrap_or("");
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2017-06-05 16:25:13 +02:00
|
|
|
// Get the current working directory
|
2017-10-14 19:57:15 +02:00
|
|
|
let current_dir = Path::new(".");
|
2017-10-22 11:47:05 +02:00
|
|
|
if !fshelper::is_dir(current_dir) {
|
2018-10-27 16:30:29 +02:00
|
|
|
print_error_and_exit!("Could not get current directory.");
|
2017-10-14 19:57:15 +02:00
|
|
|
}
|
2017-05-12 11:50:03 +02:00
|
|
|
|
2018-03-14 22:49:53 +01:00
|
|
|
// Get one or more root directories to search.
|
2018-10-20 20:24:53 +02:00
|
|
|
let mut dir_vec: Vec<_> = match matches
|
|
|
|
.values_of("path")
|
2019-01-26 02:13:16 +01:00
|
|
|
.or_else(|| matches.values_of("search-path"))
|
2018-10-20 20:24:53 +02:00
|
|
|
{
|
2018-01-01 12:16:43 +01:00
|
|
|
Some(paths) => paths
|
|
|
|
.map(|path| {
|
|
|
|
let path_buffer = PathBuf::from(path);
|
|
|
|
if !fshelper::is_dir(&path_buffer) {
|
2018-10-03 15:53:59 +02:00
|
|
|
print_error_and_exit!(
|
2018-10-27 16:30:29 +02:00
|
|
|
"'{}' is not a directory.",
|
2018-01-01 12:16:43 +01:00
|
|
|
path_buffer.to_string_lossy()
|
2018-10-03 15:53:59 +02:00
|
|
|
);
|
2018-01-01 12:16:43 +01:00
|
|
|
}
|
|
|
|
path_buffer
|
2018-09-27 23:01:38 +02:00
|
|
|
})
|
|
|
|
.collect::<Vec<_>>(),
|
2017-12-06 23:52:23 +01:00
|
|
|
None => vec![current_dir.to_path_buf()],
|
2017-06-11 20:41:32 +02:00
|
|
|
};
|
|
|
|
|
2017-12-06 23:52:23 +01:00
|
|
|
if matches.is_present("absolute-path") {
|
|
|
|
dir_vec = dir_vec
|
|
|
|
.iter()
|
2018-03-25 23:47:58 +02:00
|
|
|
.map(|path_buffer| {
|
|
|
|
path_buffer
|
|
|
|
.canonicalize()
|
|
|
|
.and_then(|pb| fshelper::absolute_path(pb.as_path()))
|
|
|
|
.unwrap()
|
2018-09-27 23:01:38 +02:00
|
|
|
})
|
|
|
|
.collect();
|
2017-10-14 19:57:15 +02:00
|
|
|
}
|
2017-06-05 16:25:13 +02:00
|
|
|
|
2018-03-14 23:13:17 +01:00
|
|
|
// 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)
|
2018-03-14 23:13:17 +01:00
|
|
|
&& fshelper::is_dir(Path::new(pattern))
|
|
|
|
{
|
2018-10-03 15:53:59 +02:00
|
|
|
print_error_and_exit!(
|
2018-10-27 16:30:29 +02:00
|
|
|
"The search pattern '{pattern}' contains a path-separation character ('{sep}') \
|
2018-03-14 23:13:17 +01:00
|
|
|
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\
|
|
|
|
Instead, if you want to search for the pattern in the full path, use:\n\n \
|
|
|
|
fd --full-path '{pattern}'",
|
|
|
|
pattern = pattern,
|
|
|
|
sep = std::path::MAIN_SEPARATOR,
|
2018-10-03 15:53:59 +02:00
|
|
|
);
|
2018-03-14 23:13:17 +01:00
|
|
|
}
|
|
|
|
|
2019-09-15 15:37:08 +02:00
|
|
|
let pattern_regex = if matches.is_present("glob") {
|
|
|
|
let glob = match Glob::new(pattern) {
|
|
|
|
Ok(glob) => glob,
|
|
|
|
Err(e) => {
|
|
|
|
print_error_and_exit!("{}", e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
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
|
|
|
|
2017-09-30 23:02:57 +02:00
|
|
|
let colored_output = match matches.value_of("color") {
|
|
|
|
Some("always") => true,
|
|
|
|
Some("never") => false,
|
2017-10-12 08:01:51 +02:00
|
|
|
_ => atty::is(Stream::Stdout),
|
2017-09-30 23:02:57 +02:00
|
|
|
};
|
2017-10-26 21:44:40 +02:00
|
|
|
|
2019-04-12 02:16:02 +02:00
|
|
|
let path_separator = matches.value_of("path-separator").map(|str| str.to_owned());
|
2019-04-11 00:30:56 +02:00
|
|
|
|
2017-10-20 09:15:32 +02:00
|
|
|
#[cfg(windows)]
|
2018-03-12 22:24:12 +01:00
|
|
|
let colored_output = colored_output && ansi_term::enable_ansi_support().is_ok();
|
2017-05-15 21:41:31 +02:00
|
|
|
|
2017-10-12 08:01:51 +02:00
|
|
|
let ls_colors = if colored_output {
|
2018-12-09 14:56:05 +01:00
|
|
|
Some(LsColors::from_env().unwrap_or_default())
|
2017-10-12 08:01:51 +02:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
2017-05-12 15:44:09 +02:00
|
|
|
|
2018-11-11 18:00:01 +01:00
|
|
|
let command = matches
|
|
|
|
.values_of("exec")
|
|
|
|
.map(CommandTemplate::new)
|
|
|
|
.or_else(|| {
|
|
|
|
matches.values_of("exec-batch").map(|m| {
|
|
|
|
CommandTemplate::new_batch(m).unwrap_or_else(|e| {
|
|
|
|
print_error_and_exit!("{}", e);
|
|
|
|
})
|
|
|
|
})
|
|
|
|
});
|
2017-10-14 18:04:11 +02:00
|
|
|
|
2018-04-23 01:38:10 +02:00
|
|
|
let size_limits: Vec<SizeFilter> = matches
|
|
|
|
.values_of("size")
|
2018-04-24 00:13:21 +02:00
|
|
|
.map(|v| {
|
|
|
|
v.map(|sf| {
|
|
|
|
if let Some(f) = SizeFilter::from_string(sf) {
|
|
|
|
return f;
|
|
|
|
}
|
2018-10-27 16:30:29 +02:00
|
|
|
print_error_and_exit!("'{}' is not a valid size constraint. See 'fd --help'.", sf);
|
2018-09-27 23:01:38 +02:00
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
})
|
|
|
|
.unwrap_or_else(|| vec![]);
|
2018-04-23 01:38:10 +02:00
|
|
|
|
2018-10-10 12:13:19 +02:00
|
|
|
let now = time::SystemTime::now();
|
|
|
|
let mut time_constraints: Vec<TimeFilter> = Vec::new();
|
2018-10-09 13:47:42 +02:00
|
|
|
if let Some(t) = matches.value_of("changed-within") {
|
2018-10-10 12:13:19 +02:00
|
|
|
if let Some(f) = TimeFilter::after(&now, t) {
|
|
|
|
time_constraints.push(f);
|
2018-10-09 13:47:42 +02:00
|
|
|
} else {
|
2018-10-27 16:30:29 +02:00
|
|
|
print_error_and_exit!("'{}' is not a valid date or duration. See 'fd --help'.", t);
|
2018-10-09 13:47:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(t) = matches.value_of("changed-before") {
|
2018-10-10 12:13:19 +02:00
|
|
|
if let Some(f) = TimeFilter::before(&now, t) {
|
|
|
|
time_constraints.push(f);
|
2018-10-09 13:47:42 +02:00
|
|
|
} else {
|
2018-10-27 16:30:29 +02:00
|
|
|
print_error_and_exit!("'{}' is not a valid date or duration. See 'fd --help'.", t);
|
2018-10-09 13:47:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-12 15:44:09 +02:00
|
|
|
let config = FdOptions {
|
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")),
|
2017-10-12 08:01:51 +02:00
|
|
|
follow_links: matches.is_present("follow"),
|
|
|
|
null_separator: matches.is_present("null_separator"),
|
2018-01-01 12:16:43 +01:00
|
|
|
max_depth: matches
|
|
|
|
.value_of("depth")
|
2018-09-13 18:19:17 +02:00
|
|
|
.or_else(|| matches.value_of("rg-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,
|
2018-02-25 11:11:14 +01:00
|
|
|
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
|
|
|
}
|
2018-08-19 17:05:04 +02:00
|
|
|
"e" | "empty" => {
|
|
|
|
file_types.empty_only = true;
|
|
|
|
}
|
2018-02-25 11:11:14 +01:00
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
2018-08-19 17:05:04 +02:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2018-02-25 11:11:14 +01:00
|
|
|
file_types
|
|
|
|
}),
|
2018-01-01 12:09:33 +01:00
|
|
|
extensions: matches.values_of("extension").map(|exts| {
|
2018-05-14 18:39:47 +02:00
|
|
|
let patterns = exts
|
2019-03-02 01:19:47 +01:00
|
|
|
.map(|e| e.trim_start_matches('.'))
|
2018-02-07 16:13:28 +01:00
|
|
|
.map(|e| format!(r".\.{}$", regex::escape(e)));
|
|
|
|
match RegexSetBuilder::new(patterns)
|
|
|
|
.case_insensitive(true)
|
|
|
|
.build()
|
|
|
|
{
|
|
|
|
Ok(re) => re,
|
2018-10-03 15:53:59 +02:00
|
|
|
Err(err) => {
|
|
|
|
print_error_and_exit!("{}", err.description());
|
|
|
|
}
|
2018-02-07 16:13:28 +01:00
|
|
|
}
|
2017-10-12 08:01:51 +02:00
|
|
|
}),
|
2019-01-29 23:09:45 +01:00
|
|
|
command: command.map(Arc::new),
|
2017-10-22 23:00:19 +02:00
|
|
|
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![]),
|
2018-04-23 01:38:10 +02:00
|
|
|
size_constraints: size_limits,
|
2018-10-10 12:13:19 +02:00
|
|
|
time_constraints,
|
2018-10-22 14:20:08 +02:00
|
|
|
show_filesystem_errors: matches.is_present("show-errors"),
|
2019-04-11 00:30:56 +02:00
|
|
|
path_separator,
|
2017-05-12 15:44:09 +02:00
|
|
|
};
|
2017-05-12 13:32:30 +02:00
|
|
|
|
2018-02-10 15:19:53 +01:00
|
|
|
match RegexBuilder::new(&pattern_regex)
|
2017-10-12 08:01:51 +02:00
|
|
|
.case_insensitive(!config.case_sensitive)
|
2017-10-14 15:41:38 +02:00
|
|
|
.dot_matches_new_line(true)
|
2018-01-01 12:16:43 +01:00
|
|
|
.build()
|
|
|
|
{
|
2019-09-13 22:26:27 +02:00
|
|
|
Ok(re) => {
|
|
|
|
let exit_code = walk::scan(&dir_vec, Arc::new(re), Arc::new(config));
|
|
|
|
process::exit(exit_code.into());
|
|
|
|
}
|
2018-10-03 15:53:59 +02:00
|
|
|
Err(err) => {
|
|
|
|
print_error_and_exit!(
|
2018-02-18 17:50:53 +01:00
|
|
|
"{}\nHint: You can use the '--fixed-strings' option to search for a \
|
2018-02-19 09:57:09 +01:00
|
|
|
literal string instead of a regular expression",
|
2018-02-18 17:50:53 +01:00
|
|
|
err.description()
|
2018-10-03 15:53:59 +02:00
|
|
|
);
|
|
|
|
}
|
2017-05-12 11:50:03 +02:00
|
|
|
}
|
|
|
|
}
|