diff --git a/CHANGELOG.md b/CHANGELOG.md index 69b22c30e..185534c8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/cli.rs b/src/cli.rs index b45ef12dc..9bef96d6b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -498,6 +498,19 @@ pub struct Opts { )] pub ignore_file: Vec, + /// 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, + /// Declare when to use color for the pattern match output #[arg( long, diff --git a/src/config.rs b/src/config.rs index 9e18120c4..271b44f37 100644 --- a/src/config.rs +++ b/src/config.rs @@ -103,6 +103,10 @@ pub struct Config { /// A list of custom ignore files. pub ignore_files: Vec, + /// 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, + /// The given constraints on the size of returned files pub size_constraints: Vec, diff --git a/src/main.rs b/src/main.rs index f8975446e..13d935fc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -314,6 +314,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result (), - 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 => (), } @@ -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;