Repository: sharkdp/fd (language: rust)

Issue:
No option to limit search to one file system
Many tools surrounding file system traversal include an option to disable crossing file system boundaries. find includes the `-xdev` option, which accomplishes this. fd, however, seems to lack such a feature at the present time. I've looked through the man page and the issue list and was unable to find anything relating to this.

Looking at the docs of the ignore crate, implementation should be quite simple as well: it only requires adding a call to `.same_file_system(config.same_file_system)` to the [`WalkBuilder`](https://github.com/sharkdp/fd/blob/master/src/walk.rs#L72), assuming a corresponding option `same_file_system: bool` is added to `FdOptions`, with corresponding changes in `build_app()` and `main()`.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index f2fda276e..6798f9a3d 100644
--- a/README.md
+++ b/README.md
@@ -285,19 +285,19 @@ USAGE:
     fd [FLAGS/OPTIONS] [<pattern>] [<path>...]
 
 FLAGS:
-    -H, --hidden            Search hidden files and directories
-    -I, --no-ignore         Do not respect .(git|fd)ignore files
-        --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)
-    -g, --glob              Glob-based search (default: regular expression)
-    -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)
-    -0, --print0            Separate results by the null character
-    -h, --help              Prints help information
-    -V, --version           Prints version information
+    -H, --hidden             Search hidden files and directories
+    -I, --no-ignore          Do not respect .(git|fd)ignore files
+        --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)
+    -g, --glob               Glob-based search (default: regular expression)
+    -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)
+    -0, --print0             Separate results by the null character
+    -h, --help               Prints help information
+    -V, --version            Prints version information
 
 OPTIONS:
     -d, --max-depth <depth>            Set maximum search depth (default: none)
diff --git a/src/app.rs b/src/app.rs
index b86169d85..8be6f9499 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -37,7 +37,7 @@ pub fn build_app() -> App<'static, 'static> {
             .long_help(helps[name].long)
     };
 
-    App::new("fd")
+    let mut app = App::new("fd")
         .version(crate_version!())
         .usage("fd [FLAGS/OPTIONS] [<pattern>] [<path>...]")
         .setting(AppSettings::ColoredHelp)
@@ -282,7 +282,21 @@ pub fn build_app() -> App<'static, 'static> {
                 .multiple(true)
                 .hidden_short_help(true)
                 .number_of_values(1),
-        )
+        );
+
+    // Make `--one-file-system` available only on Unix and Windows platforms, as per the
+    // restrictions on the corresponding option in the `ignore` crate.
+    // Provide aliases `mount` and `xdev` for people coming from `find`.
+    if cfg!(any(unix, windows)) {
+        app = app.arg(
+            arg("one-file-system")
+                .long("one-file-system")
+                .aliases(&["mount", "xdev"])
+                .hidden_short_help(true),
+        );
+    }
+
+    app
 }
 
 #[rustfmt::skip]
@@ -457,5 +471,14 @@ fn usage() -> HashMap<&'static str, Help> {
            results will be shown with respect to the given base path. Note that relative paths \
            which are passed to fd via the positional <path> argument or the '--search-path' option \
            will also be resolved relative to this directory.");
+
+    if cfg!(any(unix, windows)) {
+        doc!(h, "one-file-system"
+            , "Don't cross file system boundaries"
+            , "By default, fd will traverse the file system tree as far as other options dictate. \
+               With this flag, fd ensures that it does not descend into a different file system \
+               than the one it started in. Comparable to the -mount or -xdev filters of find(1).");
+    }
+
     h
 }
diff --git a/src/internal/opts.rs b/src/internal/opts.rs
index 1673129a0..b3feb1cbc 100644
--- a/src/internal/opts.rs
+++ b/src/internal/opts.rs
@@ -28,6 +28,9 @@ pub struct FdOptions {
     /// Whether to follow symlinks or not.
     pub follow_links: bool,
 
+    /// Whether to limit the search to starting file system or not.
+    pub one_file_system: bool,
+
     /// Whether elements of output should be separated by a null character
     pub null_separator: bool,
 
diff --git a/src/main.rs b/src/main.rs
index e17dc7d12..f80743c58 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -208,6 +208,7 @@ fn main() {
             || matches.is_present("rg-alias-hidden-ignore")
             || matches.is_present("no-ignore-vcs")),
         follow_links: matches.is_present("follow"),
+        one_file_system: matches.is_present("one-file-system"),
         null_separator: matches.is_present("null_separator"),
         max_depth: matches
             .value_of("depth")
diff --git a/src/walk.rs b/src/walk.rs
index df25666de..76ffe0123 100644
--- a/src/walk.rs
+++ b/src/walk.rs
@@ -78,6 +78,8 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) -
         .git_exclude(config.read_vcsignore)
         .overrides(overrides)
         .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 {


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index 723725266..08c453a87 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -555,6 +555,49 @@ fn test_follow() {
     );
 }
 
+// File system boundaries (--one-file-system)
+// Limited to Unix because, to the best of my knowledge, there is no easy way to test a use case
+// file systems mounted into the tree on Windows.
+// Not limiting depth causes massive delay under Darwin, see BurntSushi/ripgrep#1429
+#[test]
+#[cfg(unix)]
+fn test_file_system_boundaries() {
+    // Helper function to get the device ID for a given path
+    // Inspired by https://github.com/BurntSushi/ripgrep/blob/8892bf648cfec111e6e7ddd9f30e932b0371db68/ignore/src/walk.rs#L1693
+    fn device_num(path: impl AsRef<Path>) -> u64 {
+        use std::os::unix::fs::MetadataExt;
+
+        path.as_ref().metadata().map(|md| md.dev()).unwrap()
+    }
+
+    // Can't simulate file system boundaries
+    let te = TestEnv::new(&[], &[]);
+
+    let dev_null = Path::new("/dev/null");
+
+    // /dev/null should exist in all sane Unixes. Skip if it doesn't exist for some reason.
+    // Also skip should it be on the same device as the root partition for some reason.
+    if !dev_null.is_file() || device_num(dev_null) == device_num("/") {
+        return;
+    }
+
+    te.assert_output(
+        &["--full-path", "--max-depth", "2", "^/dev/null$", "/"],
+        "/dev/null",
+    );
+    te.assert_output(
+        &[
+            "--one-file-system",
+            "--full-path",
+            "--max-depth",
+            "2",
+            "^/dev/null$",
+            "/",
+        ],
+        "",
+    );
+}
+
 /// Null separator (--print0)
 #[test]
 fn test_print0() {