From 82485bf18aeb305cf6f427ccc05486af3b316b42 Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:51:04 -0600 Subject: [PATCH 1/7] feat: Add --exact argument to match exact filename --- src/cli.rs | 17 +++++++++++++++-- src/main.rs | 4 ++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9c54d7c2..08b735af 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -173,17 +173,30 @@ pub struct Opts { /// Treat the pattern as a literal string instead of a regular expression. Note /// that this also performs substring comparison. If you want to match on an - /// exact filename, consider using '--glob'. + /// exact filename, consider using '--glob' or '--exact' instead. #[arg( long, short = 'F', alias = "literal", hide_short_help = true, - help = "Treat pattern as literal string stead of regex", + help = "Treat pattern as literal string instead of regex", long_help )] pub fixed_strings: bool, + /// Perform an exact match. This is equivalent to '--fixed-strings' but requires + /// the pattern to match the entire filename (or path if '--full-path' is used), + /// rather than a substring. Special regex characters in the pattern are treated + /// as literal characters. + #[arg( + long, + conflicts_with("glob"), + hide_short_help = true, + help = "Match the entire filename exactly (literal, non-substring)", + long_help + )] + pub exact: bool, + /// Add additional required search patterns, all of which must be matched. Multiple /// additional patterns can be specified. The patterns are regular /// expressions, unless '--glob' or '--fixed-strings' is used. diff --git a/src/main.rs b/src/main.rs index 8b7bd936..f7041b3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -170,6 +170,10 @@ fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result { Ok(if opts.glob && !pattern.is_empty() { let glob = GlobBuilder::new(pattern).literal_separator(true).build()?; glob.regex().to_owned() + } else if opts.exact { + // Anchor the escaped pattern so the full filename (or path) must match exactly. + // Literal. No substring matching. + format!("^{}$", regex::escape(pattern)) } else if opts.fixed_strings { // Treat pattern as literal string if '--fixed-strings' is used regex::escape(pattern) From 8f5a1675f51700a4520635d162c73e89ad4135e8 Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:57:28 -0600 Subject: [PATCH 2/7] test: Add --exact test cases, add --fixed-string cases too --- tests/tests.rs | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index c125d3a5..9815dd8b 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2135,14 +2135,49 @@ fn test_fixed_strings() { // Regex search, parens are treated as group te.assert_output(&["download (1)"], ""); - // Literal search, parens are treated as characters + // Literal search, parens are treated as characters. Case-insensitive by default. te.assert_output( &["--fixed-strings", "download (1)"], "test2/Download (1).tar.gz", ); - // Combine with --case-sensitive + // Combine with --case-sensitive (unmatched). te.assert_output(&["--fixed-strings", "--case-sensitive", "download (1)"], ""); + + // Combine with --case-sensitive (matched). + te.assert_output( + &["--fixed-strings", "--case-sensitive", "Download (1)"], + "test2/Download (1).tar.gz", + ); +} + +/// Literal search, non-substring (--exact) +#[test] +fn test_exact_literal_nonsubstring() { + let dirs = &["test1", "test2"]; + let files = &["test1/a.foo", "test1/a_foo", "test2/Download (1).tar.gz"]; + let te = TestEnv::new(dirs, files); + + // Literal search, dot is treated as character + te.assert_output(&["--exact", "a.foo"], "test1/a.foo"); + + // Literal search, parens are treated as characters. Substring should not match. + te.assert_output(&["--exact", "download (1)"], ""); + + // Literal search, parens are treated as characters. Case-insensitive by default. + te.assert_output( + &["--exact", "download (1).tar.gz"], + "test2/Download (1).tar.gz", + ); + + // Combine with --case-sensitive, should not match due to case mismatch. + te.assert_output(&["--exact", "--case-sensitive", "download (1).tar.gz"], ""); + + // Combine with --case-sensitive, exact match should match. + te.assert_output( + &["--exact", "--case-sensitive", "Download (1).tar.gz"], + "test2/Download (1).tar.gz", + ); } /// Filenames with invalid UTF-8 sequences From 468c807049bee89eb8d3119ff260243c00964664 Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:31:03 -0600 Subject: [PATCH 3/7] docs: Update suggested non-regex note on regex failure --- src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index f7041b3c..cac3f37f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -496,8 +496,9 @@ fn build_regex(pattern_regex: String, config: &Config) -> Result Date: Thu, 14 May 2026 13:59:48 -0600 Subject: [PATCH 4/7] test: Should not match substring filenames --- tests/tests.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 9815dd8b..75aab1f0 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2155,10 +2155,18 @@ fn test_fixed_strings() { #[test] fn test_exact_literal_nonsubstring() { let dirs = &["test1", "test2"]; - let files = &["test1/a.foo", "test1/a_foo", "test2/Download (1).tar.gz"]; + let files = &[ + "test1/a.foo", + "test1/aa.foo", + "test1/a.food", + "test1/ca.food", + "test1/a_foo", + "test2/Download (1).tar.gz", + ]; let te = TestEnv::new(dirs, files); - // Literal search, dot is treated as character + // Literal search, dot is treated as character. Should match only the exact name, + // not "aa.foo", "a.food", or "ca.food". te.assert_output(&["--exact", "a.foo"], "test1/a.foo"); // Literal search, parens are treated as characters. Substring should not match. From d642c446c955db3fc45f74db0c1124ce4170867e Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Thu, 14 May 2026 14:03:44 -0600 Subject: [PATCH 5/7] fix: --exact should conflict with --fix-strings --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 08b735af..329c6d43 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -190,7 +190,7 @@ pub struct Opts { /// as literal characters. #[arg( long, - conflicts_with("glob"), + conflicts_with_all(["glob", "fixed_strings"]), hide_short_help = true, help = "Match the entire filename exactly (literal, non-substring)", long_help From cb2f37a46a3f6c8c85889e99a52b39b246370184 Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Thu, 14 May 2026 14:05:28 -0600 Subject: [PATCH 6/7] docs: Add --exact to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4b013bb..a0c82818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features - Add `--ignore-parent` option to override `--no-ignore-parent`, see #1958 (@tmchow) +- Add `--exact` option to match the entire filename exactly (literal, non-substring). ## Bugfixes - Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir). From 9f12a3059c8c10a54f5e6e0123cd81d0ed0a5bc6 Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Thu, 14 May 2026 14:06:02 -0600 Subject: [PATCH 7/7] docs: Add --exact to man page --- doc/fd.1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/fd.1 b/doc/fd.1 index aa167b89..5a750e5f 100644 --- a/doc/fd.1 +++ b/doc/fd.1 @@ -114,7 +114,14 @@ Perform a regular-expression based search (default). This can be used to overrid .TP .B \-F, \-\-fixed\-strings Treat the pattern as a literal string instead of a regular expression. Note that this also -performs substring comparison. If you want to match on an exact filename, consider using '\-\-glob'. +performs substring comparison. If you want to match on an exact filename, consider using '\-\-glob' +or '\-\-exact'. +.TP +.B \-\-exact +Perform an exact match. This is equivalent to '\-\-fixed\-strings' but requires the pattern to +match the entire filename (or the full path if '\-\-full\-path' is used), rather than a substring. +Special regex characters in the pattern are treated as literal characters. +Cannot be used together with '\-\-glob' or '\-\-fixed\-strings'. .TP .BI "\-\-and " pattern Add additional required search patterns, all of which must be matched. Multiple additional