Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Features

- Add a hidden `--mindepth` alias for `--min-depth`. (#1617)
- Add `--ignore-file-name` option to specify a custom name for ignore files, providing more
flexible ignore rule management. See #1713


## Bugfixes
Expand Down
13 changes: 13 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,19 @@ pub struct Opts {
)]
pub ignore_file: Vec<PathBuf>,

/// Use a custom file name for ignore files. When this is set, fd will not
/// look for '.ignore' or '.fdignore' files, but for a file with the given
/// name in each directory. If the custom ignore file is empty, all entries
/// in its directory are ignored.
#[arg(
long,
value_name = "name",
hide_short_help = true,
help = "Use a custom name for ignore files instead of .ignore/.fdignore",
long_help
)]
pub ignore_file_name: Option<String>,

/// Declare when to use color for the pattern match output
#[arg(
long,
Expand Down
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ pub struct Config {
/// A list of custom ignore files.
pub ignore_files: Vec<PathBuf>,

/// The name of the custom ignore file to look for in each directory.
/// If set, .ignore and .fdignore are not used.
pub custom_ignore_file_name: Option<String>,

/// The given constraints on the size of returned files
pub size_constraints: Vec<SizeFilter>,

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
batch_size: opts.batch_size,
exclude_patterns: opts.exclude.iter().map(|p| String::from("!") + p).collect(),
ignore_files: std::mem::take(&mut opts.ignore_file),
custom_ignore_file_name: opts.ignore_file_name.take(),
size_constraints: size_limits,
time_constraints,
#[cfg(unix)]
Expand Down
148 changes: 120 additions & 28 deletions src/walk.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::fs;
use std::io::{self, BufRead, Write};
use std::mem;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
Expand Down Expand Up @@ -350,46 +351,81 @@ impl WorkerState {
let overrides = self.build_overrides(paths)?;

let mut builder = WalkBuilder::new(first_path);

// Settings that are always applied or controlled by other specific flags
builder
.hidden(config.ignore_hidden)
.ignore(config.read_fdignore)
.parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore))
.git_ignore(config.read_vcsignore)
.git_global(config.read_vcsignore)
.git_exclude(config.read_vcsignore)
.require_git(config.require_git_to_read_vcsignore)
.overrides(overrides)
.overrides(overrides) // Apply command-line exclude patterns (--exclude)
.follow_links(config.follow_links)
// No need to check for supported platforms, option is unavailable on unsupported ones
.same_file_system(config.one_file_system)
.max_depth(config.max_depth);

if config.read_fdignore {
builder.add_custom_ignore_filename(".fdignore");
}
if let Some(custom_ignore_name) = &config.custom_ignore_file_name {
// A custom ignore file name is specified: disable other file-based ignores
builder.ignore(false); // Do not look for default ".ignore" files
builder.git_ignore(false); // Do not use .gitignore
builder.git_global(false); // Do not use global git ignore
builder.git_exclude(false); // Do not use .git/info/exclude
// .require_git(false) might also be set here, but if git_ignore is false, it may not be needed.
builder.parents(config.read_parent_ignore); // Governs if custom_ignore_name is sought in parent dirs
builder.add_custom_ignore_filename(custom_ignore_name);
} else {
// Default ignore file behavior
builder.ignore(config.read_fdignore); // Look for ".ignore" if read_fdignore is true
builder.parents(config.read_parent_ignore && (config.read_fdignore || config.read_vcsignore));
builder.git_ignore(config.read_vcsignore);
builder.git_global(config.read_vcsignore);
builder.git_exclude(config.read_vcsignore);
builder.require_git(config.require_git_to_read_vcsignore);

if config.read_fdignore {
builder.add_custom_ignore_filename(".fdignore");
}

if config.read_global_ignore {
if let Ok(basedirs) = etcetera::choose_base_strategy() {
let global_ignore_file = basedirs.config_dir().join("fd").join("ignore");
if global_ignore_file.is_file() {
let result = builder.add_ignore(global_ignore_file);
match result {
Some(ignore::Error::Partial(_)) => (),
Some(err) => {
print_error(format!("Malformed pattern in global ignore file. {err}."));
if config.read_global_ignore {
if let Ok(basedirs) = etcetera::choose_base_strategy() {
let global_ignore_file = basedirs.config_dir().join("fd").join("ignore");
if global_ignore_file.is_file() {
let result = builder.add_ignore(&global_ignore_file); // Pass by reference
match result {
Some(ignore::Error::Partial(partial_err)) => {
print_error(format!(
"Partially malformed pattern in global ignore file {}: {:?}.",
global_ignore_file.display(),
partial_err
));
}
Some(err) => {
print_error(format!(
"Malformed pattern in global ignore file {}: {:?}.",
global_ignore_file.display(),
err
));
}
None => (),
}
None => (),
}
}
}
}

for ignore_file in &config.ignore_files {
let result = builder.add_ignore(ignore_file);
// Add ignore files specified with --ignore-file (these are explicit paths from CLI)
for ignore_file_path in &config.ignore_files {
let result = builder.add_ignore(ignore_file_path); // Pass by reference
match result {
Some(ignore::Error::Partial(_)) => (),
Some(ignore::Error::Partial(partial_err)) => {
print_error(format!(
"Partially malformed pattern in custom ignore file {}: {:?}.",
ignore_file_path.display(),
partial_err
));
}
Some(err) => {
print_error(format!("Malformed pattern in custom ignore file. {err}."));
print_error(format!(
"Malformed pattern in custom ignore file {}: {}.",
ignore_file_path.display(),
err
));
}
None => (),
}
Expand Down Expand Up @@ -457,12 +493,68 @@ impl WorkerState {
}
let mut tx = BatchSender::new(tx.clone(), limit);

Box::new(move |entry| {
Box::new(move |entry_result| {
if quit_flag.load(Ordering::Relaxed) {
return WalkState::Quit;
}

let entry = match entry {
// Handle empty custom ignore file: if a custom ignore file name is specified,
// and a file with that name exists in the current entry's directory (if entry is dir)
// or parent directory (if entry is file), and that file is empty (modulo comments/whitespace),
// then skip this entry and do not recurse if it's a directory.
if let Some(custom_ignore_name) = &config.custom_ignore_file_name {
if let Ok(ref live_entry) = entry_result { // Process only if entry is not an error
let path_of_entry = live_entry.path();
let mut dir_to_check_for_ignore_file = PathBuf::new();

if live_entry.file_type().map_or(false, |ft| ft.is_dir()) {
dir_to_check_for_ignore_file.push(path_of_entry);
} else if let Some(parent) = path_of_entry.parent() {
dir_to_check_for_ignore_file.push(parent);
} else {
// Should not happen for entries from WalkParallel, but handle defensively
dir_to_check_for_ignore_file.push(".");
}

let custom_ignore_file_on_disk = dir_to_check_for_ignore_file.join(custom_ignore_name);

if custom_ignore_file_on_disk.is_file() {
// TODO: Consider caching the empty-check result per directory path
// to avoid repeated file I/O and parsing for entries in the same directory.
// For now, re-evaluate each time for simplicity.
let mut is_truly_empty = true;
match fs::File::open(&custom_ignore_file_on_disk) {
Ok(file) => {
let reader = io::BufReader::new(file);
for line_res in reader.lines() {
if let Ok(line) = line_res {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
is_truly_empty = false;
break;
}
} else { // Error reading a line
is_truly_empty = false;
break;
}
}
}
Err(_e) => { // File could not be opened (e.g., permissions, or disappeared)
is_truly_empty = false;
// Optionally log error: print_error(format!("Could not open custom ignore file {}: {}", custom_ignore_file_on_disk.display(), _e));
}
}

if is_truly_empty {
// If the custom ignore file in this entry's effective directory is empty,
// skip this entry. If this entry is a directory, WalkState::Skip also prevents recursion.
return WalkState::Skip;
}
}
}
}

let entry = match entry_result {
Ok(ref e) if e.depth() == 0 => {
// Skip the root directory entry.
return WalkState::Continue;
Expand Down
Loading