This commit is contained in:
cyqsimon 2023-06-14 21:18:12 +02:00 committed by GitHub
commit f8979bb6b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 18 deletions

View File

@ -113,6 +113,7 @@ _fd() {
'*'{-t+,--type=}"[filter search by type]:type:(($fd_types))"
'*'{-e+,--extension=}'[filter search by file extension]:extension'
'*'{-E+,--exclude=}'[exclude files/directories that match the given glob pattern]:glob pattern'
'*--exclude-absolute=[exclude files/directories whose absolute path match the given glob pattern]:glob pattern'
'*'{-S+,--size=}'[limit search by file size]:size limit:->size'
'(-o --owner)'{-o+,--owner=}'[filter by owning user and/or group]:owner and/or group:->owner'

View File

@ -46,7 +46,7 @@ pub struct Opts {
no_hidden: (),
/// Show search results from files and directories that would otherwise be
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file.
/// ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore files.
/// The flag can be overridden with --ignore.
#[arg(
long,
@ -106,7 +106,7 @@ pub struct Opts {
)]
pub no_ignore_parent: bool,
/// Do not respect the global ignore file
/// Do not respect the global ignore files
#[arg(long, hide = true)]
pub no_global_ignore_file: bool,
@ -300,6 +300,21 @@ pub struct Opts {
)]
pub exclude: Vec<String>,
/// Exclude files/directories whose absolute path match the given glob pattern.
/// This filter is applied on top of all other ignore logic. Multiple exclude patterns
/// can be specified.
///
/// Note that using this filter causes fd to perform an extra canonicalization
/// for every path traversed, which incurs a non-trivial performance penalty.
/// Use at your own discretion.
#[arg(
long,
value_name = "pattern",
help = "Exclude entries whose absolute path match the given glob pattern",
long_help
)]
pub exclude_absolute: Vec<String>,
/// Do not traverse into directories that match the search criteria. If
/// you want to exclude specific directories, use the '--exclude=…' option.
#[arg(long, hide_short_help = true, conflicts_with_all(&["size", "exact_depth"]),

View File

@ -1,5 +1,6 @@
use std::{path::PathBuf, sync::Arc, time::Duration};
use globset::GlobMatcher;
use lscolors::LsColors;
use regex::bytes::RegexSet;
@ -95,6 +96,9 @@ pub struct Config {
/// A list of glob patterns that should be excluded from the search.
pub exclude_patterns: Vec<String>,
/// A list of glob matchers that should exclude matched entries by their absolute paths.
pub exclude_absolute_matchers: Vec<GlobMatcher>,
/// A list of custom ignore files.
pub ignore_files: Vec<PathBuf>,
@ -130,3 +134,19 @@ impl Config {
self.command.is_none()
}
}
/// Get the platform-specific config directory for fd.
pub fn get_fd_config_dir() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
let mut dir = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| dirs_next::home_dir().map(|d| d.join(".config")))?;
#[cfg(not(target_os = "macos"))]
let mut dir = dirs_next::config_dir()?;
dir.push("fd");
Some(dir)
}

View File

@ -19,12 +19,13 @@ use std::time;
use anyhow::{anyhow, bail, Context, Result};
use clap::{CommandFactory, Parser};
use globset::GlobBuilder;
use globset::{Glob, GlobBuilder, GlobMatcher};
use lscolors::LsColors;
use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder};
use crate::cli::{ColorWhen, Opts};
use crate::config::Config;
use crate::config::{get_fd_config_dir, Config};
use crate::error::print_error;
use crate::exec::CommandSet;
use crate::exit_codes::ExitCode;
use crate::filetypes::FileTypes;
@ -234,6 +235,28 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
let command = extract_command(&mut opts, colored_output)?;
let has_command = command.is_some();
let read_global_ignore =
!(opts.no_ignore || opts.rg_alias_ignore() || opts.no_global_ignore_file);
let exclude_absolute_matchers = {
let mut matchers = vec![];
// absolute excludes from CLI
for glob_str in opts.exclude_absolute.iter() {
// invalid globs from CLI are hard errors
let m = Glob::new(glob_str)?.compile_matcher();
matchers.push(m);
}
// absolute excludes from global `ignore-absolute` file
if read_global_ignore {
match read_and_build_global_exclude_absolute_matchers() {
Ok(mut v) => matchers.append(&mut v),
Err(err) => print_error(format!("Cannot read global ignore-absolute file. {err}.")),
}
}
matchers
};
Ok(Config {
case_sensitive,
search_full_path: opts.full_path,
@ -242,9 +265,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
read_vcsignore: !(opts.no_ignore || opts.rg_alias_ignore() || opts.no_ignore_vcs),
require_git_to_read_vcsignore: !opts.no_require_git,
read_parent_ignore: !opts.no_ignore_parent,
read_global_ignore: !(opts.no_ignore
|| opts.rg_alias_ignore()
|| opts.no_global_ignore_file),
read_global_ignore,
follow_links: opts.follow,
one_file_system: opts.one_file_system,
null_separator: opts.null_separator,
@ -298,6 +319,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
command: command.map(Arc::new),
batch_size: opts.batch_size,
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
exclude_absolute_matchers,
ignore_files: std::mem::take(&mut opts.ignore_file),
size_constraints: size_limits,
time_constraints,
@ -469,3 +491,44 @@ fn build_regex(pattern_regex: String, config: &Config) -> Result<regex::bytes::R
)
})
}
fn read_and_build_global_exclude_absolute_matchers() -> Result<Vec<GlobMatcher>> {
let file_content = match get_fd_config_dir()
.map(|p| p.join("ignore-absolute"))
.filter(|p| p.is_file())
{
Some(path) => std::fs::read_to_string(path)?,
// not an error if the file doesn't exist
None => return Ok(vec![]),
};
let matchers = file_content
.lines()
// trim trailing spaces, unless escaped with backslash (`\`)
.map(|raw| {
let naive_trimmed = raw.trim_end_matches(' ');
if raw.len() == naive_trimmed.len() {
raw
} else if naive_trimmed.ends_with('\\') {
&raw[..naive_trimmed.len() + 1]
} else {
naive_trimmed
}
})
// skip empty lines and comments
.filter(|line| !line.is_empty() && !line.starts_with('#'))
// build matchers
.filter_map(|line| match Glob::new(line) {
Ok(glob) => Some(glob.compile_matcher()),
// invalid globs from config file are warnings
Err(err) => {
print_error(format!(
"Malformed pattern in global ignore-absolute file. {err}."
));
None
}
})
.collect();
Ok(matchers)
}

View File

@ -14,6 +14,7 @@ use ignore::overrides::OverrideBuilder;
use ignore::{self, WalkBuilder};
use regex::bytes::Regex;
use crate::config::get_fd_config_dir;
use crate::config::Config;
use crate::dir_entry::DirEntry;
use crate::error::print_error;
@ -89,17 +90,8 @@ pub fn scan(paths: &[PathBuf], patterns: Arc<Vec<Regex>>, config: Arc<Config>) -
}
if config.read_global_ignore {
#[cfg(target_os = "macos")]
let config_dir_op = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| dirs_next::home_dir().map(|d| d.join(".config")));
#[cfg(not(target_os = "macos"))]
let config_dir_op = dirs_next::config_dir();
if let Some(global_ignore_file) = config_dir_op
.map(|p| p.join("fd").join("ignore"))
if let Some(global_ignore_file) = get_fd_config_dir()
.map(|p| p.join("ignore"))
.filter(|p| p.is_file())
{
let result = walker.add_ignore(global_ignore_file);
@ -529,6 +521,36 @@ fn spawn_senders(
}
}
// Exclude by absolute path
// `ignore` crate does not intend to support this, so it's implemented here independently
// see https://github.com/BurntSushi/ripgrep/issues/2366
// This is done last because canonicalisation has non-trivial cost
if !config.exclude_absolute_matchers.is_empty() {
match entry_path.canonicalize() {
Ok(path) => {
if config
.exclude_absolute_matchers
.iter()
.any(|glob| glob.is_match(&path))
{
// Ideally we want to return `WalkState::Skip` to emulate gitignore's
// behavior of skipping any matched directory entirely
// Unfortunately this will make the search behaviour inconsistent
// because this filter happens outside of the directory walker
//
// E.g. Given directory structure `/foo/bar/` and CWD `/`:
// - `fd --exclude-absolute '/foo'` will return nothing
// - `fd --exclude-absolute '/foo' bar` will return '/foo/bar'
// Obviously this makes no sense
return ignore::WalkState::Continue;
}
}
Err(err) => {
print_error(format!("Cannot canonicalize {entry_path:?}. {err}."));
}
}
}
if config.is_printing() {
if let Some(ls_colors) = &config.ls_colors {
// Compute colors in parallel