Repository: sharkdp/fd (language: rust)

Issue:
Allow for exact find
First of all thank you for this. It's really fast and i'm already loving it!:)

I think `fd` should allow for exact find probably using some flag. A use case i can use to explain is when searching for `error_log` files.

Using `fd error_log` returns:

```
/some/folder/open_basedir_error_log.phpt
/another/folder/open_basedir_error_log_variation.phpt
/new/folder/name/random_string_before_error_log.old
/new/folder/name/random_string_before_error_log.temp
/new/folder/name/jibberish_error_log_readme_first.txt
```

You can see how for example you can't use `xargs` to delete as you might delete files that are actually in use by something else.

Thanks.


Discussion / hints:
> First of all thank you for this. It's really fast and i'm already loving it!:)

Thank you for the feedback!

> I think fd should allow for exact find probably using some flag. A use case i can use to explain is when searching for error_log files.

Note that the search pattern in *fd* is always a regular expression, so in a situation like this, I just use:

``` bash
> fd '^error_log$'
```

What do you think?
for comparison, ripgrep has `-F` or `--fixed-strings` flag to treat the pattern as a literal string instead of regular expression. When this flag is used, the special regular expression meta characters such as `(){}*+.` do not need to be escaped.

There's also `-g, --glob <GLOB>...`  to include/exclude files for searching in ripgrep. It's using glob pattern instead of regular expression. You can do exact file name matching using this.
Agreed, `-F`/`--fixed-strings` sounds like a useful addition.

Concerning `--glob`: @iology implemented a version of this in his fork here: https://github.com/jakwings/ff-find
> Note that the search pattern in fd is always a regular expression, so in a situation like this, I just use:

> `fd '^error_log$'`

I think your option is clever enough. However, while i had to open an issue for this (after reading the help and other issues) i wonder if this won't happen in the future. While providing a new flag to use this might seem a little more work -- i think in the long run it'll just be more easier to use and discover this..

About the pros and cons of a glob pattern compared to a fixed string: The special chars `* ? { } \` in glob patterns are seldom used in pathnames (except `[ ]`). Other than this glob patterns are more powerful than fixed strings while still safer to use than regex patterns.

One biggest problem of *fd* and my *ff* is that it does not warn users about the magic of regex. (I'll suggest people to use --glob and make it the default.)

For better interactive experience I would suggest [fzf](https://github.com/junegunn/fzf). 😉 
I don't think searching for a fixed string is worth adding an extra option here. It is easier to memorize that fd always search by regex, and that ^xxx$ means an exact string in regex, than to memorize an extra option.
Just to clarify, **fixed string** in this context isn't equal to **exact find**. Fixed string just means a regular searching without treating regex meta character as special.
Thanks for the clarification.

> for comparison, ripgrep has -F or --fixed-strings flag to treat the pattern as a literal string instead of regular expression. When this flag is used, the special regular expression meta characters such as `(){}*+.` do not need to be escaped.

However, do we actually use `(){}*` in file names? The only useful characters seem to be `.[]`. I am not sure whether this is worth introducing an extra kind of pattern.

This flag is much more useful for ripgrep, since it searches file contents, rather than file names.
At least `()` is used quite often. For example if you download and save a file with the same filename in Firefox, you get:
```
file.txt
file(1).txt
file(2).txt
```

Fix patch:
diff --git a/README.md b/README.md
index 54ac02ef1..2025528df 100644
--- a/README.md
+++ b/README.md
@@ -211,6 +211,7 @@ FLAGS:
         --no-ignore-vcs     Do not respect .gitignore files
     -s, --case-sensitive    Case-sensitive search (default: smart case)
     -i, --ignore-case       Case-insensitive search (default: smart case)
+    -F, --fixed-strings     Treat the pattern as a literal string
     -a, --absolute-path     Show absolute instead of relative paths
     -L, --follow            Follow symbolic links
     -p, --full-path         Search full path (default: file-/dirname only)
@@ -222,7 +223,7 @@ OPTIONS:
     -d, --max-depth <depth>       Set maximum search depth (default: none)
     -t, --type <filetype>...      Filter by type: f(ile), d(irectory), (sym)l(ink)
     -e, --extension <ext>...      Filter by file extension
-    -x, --exec <cmd>...           Execute a command for each search result
+    -x, --exec <cmd>              Execute a command for each search result
     -E, --exclude <pattern>...    Exclude entries that match the given glob pattern
     -c, --color <when>            When to use colors: never, *auto*, always
     -j, --threads <num>           Set number of threads to use for searching & executing
diff --git a/doc/fd.1 b/doc/fd.1
index 50083c261..613edc981 100644
--- a/doc/fd.1
+++ b/doc/fd.1
@@ -47,6 +47,9 @@ Perform a case-sensitive search (default: smart case).
 .B \-i, \-\-ignore\-case
 Perform a case-insensitive search (default: smart case).
 .TP
+.B \-F, \-\-fixed\-strings
+Treat the pattern as a literal string instead of a regular expression.
+.TP
 .B \-a, \-\-absolute\-path
 Show absolute instead of relative paths.
 .TP
diff --git a/src/app.rs b/src/app.rs
index 4c255d033..25771cccb 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -61,6 +61,12 @@ pub fn build_app() -> App<'static, 'static> {
                 .short("i")
                 .overrides_with("case-sensitive"),
         )
+        .arg(
+            arg("fixed-strings")
+                .long("fixed-strings")
+                .short("F")
+                .alias("literal"),
+        )
         .arg(arg("absolute-path").long("absolute-path").short("a"))
         .arg(arg("follow").long("follow").short("L").alias("dereference"))
         .arg(arg("full-path").long("full-path").short("p"))
@@ -153,6 +159,9 @@ fn usage() -> HashMap<&'static str, Help> {
         , "Case-insensitive search (default: smart case)"
         , "Perform a case-insensitive search. By default, fd uses case-insensitive searches, \
            unless the pattern contains an uppercase character (smart case).");
+    doc!(h, "fixed-strings"
+        , "Treat the pattern as a literal string"
+        , "Treat the pattern as a literal string instead of a regular expression.");
     doc!(h, "absolute-path"
         , "Show absolute instead of relative paths"
         , "Shows the full path starting from the root as opposed to relative paths.");
diff --git a/src/main.rs b/src/main.rs
index bd714f1c1..bff176633 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -81,10 +81,17 @@ fn main() {
             .collect();
     }
 
+    // Treat pattern as literal string if '--fixed-strings' is used
+    let pattern_regex = if matches.is_present("fixed-strings") {
+        regex::escape(pattern)
+    } else {
+        String::from(pattern)
+    };
+
     // The search will be case-sensitive if the command line flag is set or
     // if the pattern has an uppercase character (smart case).
     let case_sensitive = !matches.is_present("ignore-case")
-        && (matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern));
+        && (matches.is_present("case-sensitive") || pattern_has_uppercase_char(&pattern_regex));
 
     let colored_output = match matches.value_of("color") {
         Some("always") => true,
@@ -169,7 +176,7 @@ fn main() {
             .unwrap_or_else(|| vec![]),
     };
 
-    match RegexBuilder::new(pattern)
+    match RegexBuilder::new(&pattern_regex)
         .case_insensitive(!config.case_sensitive)
         .dot_matches_new_line(true)
         .build()


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index 2e7e2b8ab..4259c1aa0 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -791,3 +791,33 @@ fn assert_exec_output(exec_style: &str) {
         te.assert_output(&["e1", exec_style, "printf", "%s.%s\n"], "e1 e2.");
     }
 }
+
+/// Literal search (--fixed-strings)
+#[test]
+fn test_fixed_strings() {
+    let dirs = &["test1", "test2"];
+    let files = &["test1/a.foo", "test1/a_foo", "test2/Download (1).tar.gz"];
+    let te = TestEnv::new(dirs, files);
+
+    // Regex search, dot is treated as "any character"
+    te.assert_output(
+        &["a.foo"],
+        "test1/a.foo
+         test1/a_foo",
+    );
+
+    // Literal search, dot is treated as character
+    te.assert_output(&["--fixed-strings", "a.foo"], "test1/a.foo");
+
+    // Regex search, parens are treated as group
+    te.assert_output(&["download (1)"], "");
+
+    // Literal search, parens are treated as characters
+    te.assert_output(
+        &["--fixed-strings", "download (1)"],
+        "test2/Download (1).tar.gz",
+    );
+
+    // Combine with --case-sensitive
+    te.assert_output(&["--fixed-strings", "--case-sensitive", "download (1)"], "");
+}