From 7eec45aa6fc62b868f2ab53db2e190cb9a230fe7 Mon Sep 17 00:00:00 2001
From: Adam Eury <euryadam@gmail.com>
Date: Wed, 11 Sep 2024 03:00:35 -0400
Subject: [PATCH] Ignore hidden files and directories by default

---
 src/lib.rs   | 37 +++++++++++++++++++++++++++++++------
 src/main.rs  |  6 +++++-
 tests/cli.rs | 39 +++++++++++++++++++++++++++++++++++++--
 3 files changed, 73 insertions(+), 9 deletions(-)

diff --git a/src/lib.rs b/src/lib.rs
index d817d8e..f245bc3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,7 +1,13 @@
 //! Find large files on your system.
 mod bytes;
 
-use std::{collections::BTreeMap, fmt::Display, fs::ReadDir, io, path::PathBuf};
+use std::{
+    collections::BTreeMap,
+    fmt::Display,
+    fs::ReadDir,
+    io,
+    path::{Path, PathBuf},
+};
 
 /// Returns the top `n` largest files under the provided path.
 ///
@@ -14,13 +20,17 @@ use std::{collections::BTreeMap, fmt::Display, fs::ReadDir, io, path::PathBuf};
 /// ```
 /// use spacehog::find_top_n_largest_files;
 ///
-/// let results = find_top_n_largest_files("testdata", 5).unwrap();
+/// let results = find_top_n_largest_files("testdata", 5, false).unwrap();
 ///
 /// assert_eq!(results.len(), 4);
 /// ```
-pub fn find_top_n_largest_files(path: &str, n: usize) -> io::Result<Vec<(FileSize, PathBuf)>> {
+pub fn find_top_n_largest_files(
+    path: &str,
+    n: usize,
+    ignore_hidden: bool,
+) -> io::Result<Vec<(FileSize, PathBuf)>> {
     let mut results = BTreeMap::new();
-    for entry in find_files_in_path(path)? {
+    for entry in find_files_in_path(path, ignore_hidden)? {
         results.insert(entry.clone(), entry);
     }
     Ok(results.into_values().rev().take(n).collect())
@@ -36,12 +46,16 @@ impl Display for FileSize {
     }
 }
 
-fn find_files_in_path(path: &str) -> io::Result<FileIter> {
+fn find_files_in_path(path: &str, ignore_hidden: bool) -> io::Result<FileIter> {
     let dir = std::fs::read_dir(path)?;
-    Ok(FileIter { stack: vec![dir] })
+    Ok(FileIter {
+        ignore_hidden,
+        stack: vec![dir],
+    })
 }
 
 struct FileIter {
+    ignore_hidden: bool,
     stack: Vec<ReadDir>,
 }
 
@@ -54,6 +68,9 @@ impl Iterator for FileIter {
             if let Some(entry) = dir.next() {
                 let entry = entry.ok()?;
                 let path = entry.path();
+                if self.ignore_hidden && is_hidden_path(&path) {
+                    continue;
+                }
                 if path.is_dir() {
                     self.stack.push(std::fs::read_dir(path).ok()?);
                 } else {
@@ -67,6 +84,14 @@ impl Iterator for FileIter {
     }
 }
 
+fn is_hidden_path<P: AsRef<Path>>(path: P) -> bool {
+    if let Some(name) = path.as_ref().file_name() {
+        name.to_str().map_or(false, |s| s.starts_with('.'))
+    } else {
+        false
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use crate::FileSize;
diff --git a/src/main.rs b/src/main.rs
index 671cf1d..94bcfde 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,13 +11,17 @@ struct Args {
 
     #[arg(short, default_value_t = 5)]
     number: usize,
+
+    #[arg(long, default_value_t = false)]
+    hidden: bool,
 }
 
 fn main() -> anyhow::Result<()> {
     let args = Args::parse();
+    let ignore_hidden = !args.hidden;
     let mut sp = Spinner::new(spinners::Dots, "Scanning files...", Color::Blue);
 
-    let results = find_top_n_largest_files(&args.path, args.number)?;
+    let results = find_top_n_largest_files(&args.path, args.number, ignore_hidden)?;
     sp.clear();
 
     if results.is_empty() {
diff --git a/tests/cli.rs b/tests/cli.rs
index 45366a2..c547acc 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -1,7 +1,8 @@
-use std::io::BufRead;
+use std::io::Write;
+use std::{fs::File, io::BufRead};
 
 use assert_cmd::Command;
-use tempfile::TempDir;
+use tempfile::{tempdir, tempdir_in, TempDir};
 
 #[test]
 fn binary_with_version_flag_prints_version() {
@@ -80,6 +81,40 @@ fn binary_reports_that_the_directory_is_empty_if_it_contains_zero_files() {
         .stdout(predicates::str::contains("No files found."));
 }
 
+#[test]
+fn binary_ignores_hidden_files_and_directories_by_default() {
+    use std::io::Write;
+    let parent_dir = tempdir().expect("failed to create temporary directory");
+    let hidden_dir = tempdir_in(parent_dir.path()).expect("failed to create temporary directory");
+    let temp_file_path = hidden_dir.path().join("test.txt");
+    let mut temp_file = File::create(temp_file_path).expect("failed to create temporary file");
+    write!(temp_file, "hello test").unwrap();
+
+    Command::cargo_bin("spacehog")
+        .unwrap()
+        .arg(parent_dir.path())
+        .assert()
+        .success()
+        .stdout(predicates::str::contains("No files found."));
+}
+
+#[test]
+fn binary_includes_hidden_files_and_directories_when_given_hidden_flag() {
+    let parent_dir = tempdir().expect("failed to create temporary directory");
+    let hidden_dir = tempdir_in(parent_dir.path()).expect("failed to create temporary directory");
+    let temp_file_path = hidden_dir.path().join("test.txt");
+    let mut temp_file = File::create(temp_file_path).expect("failed to create temporary file");
+    write!(temp_file, "hello test").unwrap();
+
+    Command::cargo_bin("spacehog")
+        .unwrap()
+        .arg(parent_dir.path())
+        .arg("--hidden")
+        .assert()
+        .success()
+        .stdout(predicates::str::contains("*** Top 1 largest files ***"));
+}
+
 #[test]
 fn binary_with_invalid_path_arg_prints_an_error_message_and_exits_with_failure_code() {
     #[cfg(windows)]