From d21b0ffbc19edd90366c189303eeaa7a8be4a0ff Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 05:33:54 +0000 Subject: [PATCH 1/4] Add quiet mode option (-q/--quiet) This commit adds a new quiet mode option that suppresses all output except errors during processing. When quiet mode is enabled: - No informational messages are displayed (file counts, directory lists, moved file notifications, success messages) - Confirmation prompt is automatically skipped (same as -y/--yes option) - Only error messages are shown via stderr The README has been updated to document this new option with usage examples. --- README.md | 7 ++++++ src/main.rs | 64 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 81a46c6..0011425 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ OPTIONS Skip confirmation prompt and proceed immediately with the flatten operation. + -q, --quiet + Quiet mode - suppress all output except errors. Confirmation + prompt is automatically skipped (same behavior as --yes). + -i, --include Include only directories that begin with any of these values. Accepts comma-separated values. Uses case-insensitive prefix @@ -50,6 +54,9 @@ EXAMPLES Skip confirmation prompt: rflatten -y /path/to/directory + Quiet mode (no output except errors): + rflatten -q /path/to/directory + Only flatten first level subdirectories: rflatten --depth 1 /path/to/directory diff --git a/src/main.rs b/src/main.rs index e8e6a01..504b2c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,10 @@ struct Cli { #[arg(short = 'y', long = "yes")] skip_confirmation: bool, + /// Quiet mode - suppress all output except errors + #[arg(short = 'q', long = "quiet")] + quiet: bool, + /// Include only directories that start with these patterns (comma-separated) #[arg(short = 'i', long = "include", value_delimiter = ',')] include: Option>, @@ -184,6 +188,7 @@ fn flatten_directory_by_traversal( max_depth: Option, include: &Option>, exclude: &Option>, + quiet: bool, ) -> io::Result { let mut moved_count = 0; @@ -196,6 +201,7 @@ fn flatten_directory_by_traversal( exclude, &mut moved_count, None, + quiet, )?; Ok(moved_count) @@ -210,6 +216,7 @@ fn flatten_directory_by_traversal_recursive( exclude: &Option>, moved_count: &mut usize, top_level_dir: Option, + quiet: bool, ) -> io::Result<()> { if let Some(max) = max_depth { if current_depth > max { @@ -250,6 +257,7 @@ fn flatten_directory_by_traversal_recursive( exclude, moved_count, new_top_level_dir, + quiet, )?; } else if file_type.is_file() { // Only move files that are in subdirectories (not in root) @@ -287,7 +295,9 @@ fn flatten_directory_by_traversal_recursive( match fs::rename(&path, &dest) { Ok(_) => { *moved_count += 1; - println!("Moved: {} -> {}", display_path(&path), display_path(&dest)); + if !quiet { + println!("Moved: {} -> {}", display_path(&path), display_path(&dest)); + } } Err(e) => { eprintln!("Error moving {}: {}", display_path(&path), e); @@ -332,27 +342,32 @@ fn main() -> io::Result<()> { )?; if summary.file_count == 0 { - println!("No files found in subdirectories to flatten."); + if !cli.quiet { + println!("No files found in subdirectories to flatten."); + } return Ok(()); } // Show summary and get confirmation - println!( - "Found {} file(s) to move to '{}'", - summary.file_count, - display_path(&canonical_directory) - ); - - if !summary.top_level_dirs.is_empty() { - println!("Top-level directories to be flattened:"); - let mut dirs: Vec<_> = summary.top_level_dirs.iter().cloned().collect(); - dirs.sort(); - for dir in dirs { - println!(" - {}", dir); + if !cli.quiet { + println!( + "Found {} file(s) to move to '{}'", + summary.file_count, + display_path(&canonical_directory) + ); + + if !summary.top_level_dirs.is_empty() { + println!("Top-level directories to be flattened:"); + let mut dirs: Vec<_> = summary.top_level_dirs.iter().cloned().collect(); + dirs.sort(); + for dir in dirs { + println!(" - {}", dir); + } } } - if !cli.skip_confirmation { + // Skip confirmation if -y or -q is provided + if !cli.skip_confirmation && !cli.quiet { if !get_confirmation()? { println!("Flatten cancelled."); return Ok(()); @@ -365,9 +380,12 @@ fn main() -> io::Result<()> { cli.max_depth, &cli.include, &cli.exclude, + cli.quiet, )?; - println!("\nSuccessfully moved {} file(s)", moved_count); + if !cli.quiet { + println!("\nSuccessfully moved {} file(s)", moved_count); + } // Delete the now-empty top-level directories for dir in &summary.top_level_dirs { @@ -628,7 +646,7 @@ mod tests { fs::write(subdir.join("test1.txt"), "content1").unwrap(); fs::write(subdir.join("test2.txt"), "content2").unwrap(); - let moved_count = flatten_directory_by_traversal(root, None, &None, &None).unwrap(); + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, false).unwrap(); assert_eq!(moved_count, 2); assert!(root.join("test1.txt").exists()); @@ -656,7 +674,7 @@ mod tests { fs::create_dir(&subdir).unwrap(); fs::write(subdir.join("test.txt"), "subdir content").unwrap(); - let moved_count = flatten_directory_by_traversal(root, None, &None, &None).unwrap(); + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, false).unwrap(); assert_eq!(moved_count, 1); // Original file should remain unchanged @@ -690,7 +708,7 @@ mod tests { fs::create_dir(&subdir2).unwrap(); fs::write(subdir2.join("test.txt"), "content2").unwrap(); - let moved_count = flatten_directory_by_traversal(root, None, &None, &None).unwrap(); + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, false).unwrap(); assert_eq!(moved_count, 2); assert!(root.join("test.txt").exists()); @@ -704,7 +722,7 @@ mod tests { let root = temp_dir.path(); create_test_structure(root).unwrap(); - let moved_count = flatten_directory_by_traversal(root, Some(2), &None, &None).unwrap(); + let moved_count = flatten_directory_by_traversal(root, Some(2), &None, &None, false).unwrap(); // Should only move files at depths 1 and 2 assert_eq!(moved_count, 2); @@ -721,7 +739,7 @@ mod tests { create_multi_dir_structure(root).unwrap(); let include = Some(vec!["src".to_string()]); - let moved_count = flatten_directory_by_traversal(root, None, &include, &None).unwrap(); + let moved_count = flatten_directory_by_traversal(root, None, &include, &None, false).unwrap(); // Should only move files from "src" directory assert_eq!(moved_count, 1); @@ -737,7 +755,7 @@ mod tests { create_multi_dir_structure(root).unwrap(); let exclude = Some(vec!["src".to_string()]); - let moved_count = flatten_directory_by_traversal(root, None, &None, &exclude).unwrap(); + let moved_count = flatten_directory_by_traversal(root, None, &None, &exclude, false).unwrap(); // Should move all files except from "src" directory assert_eq!(moved_count, 3); @@ -752,7 +770,7 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let root = temp_dir.path(); - let moved_count = flatten_directory_by_traversal(root, None, &None, &None).unwrap(); + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, false).unwrap(); assert_eq!(moved_count, 0); } } From 6aacf21e1186b8991ecdd2e20be83761f809fe45 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 05:40:51 +0000 Subject: [PATCH 2/4] Add comprehensive tests for quiet mode option Added 6 new test cases to verify quiet mode functionality: 1. test_flatten_quiet_mode_basic - Verifies basic file moving works in quiet mode 2. test_flatten_quiet_mode_with_conflicts - Tests filename conflict resolution in quiet mode 3. test_flatten_quiet_mode_with_depth - Tests depth limiting works with quiet mode 4. test_flatten_quiet_mode_with_include_filter - Tests include filters work with quiet mode 5. test_flatten_quiet_mode_with_exclude_filter - Tests exclude filters work with quiet mode 6. test_flatten_quiet_vs_normal_same_result - Ensures quiet and normal modes produce identical file operations These tests ensure that quiet mode only affects output (suppressing stdout) while maintaining all functionality for file operations, error handling, and filtering. --- src/main.rs | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/main.rs b/src/main.rs index 504b2c4..8ea3915 100644 --- a/src/main.rs +++ b/src/main.rs @@ -773,4 +773,161 @@ mod tests { let moved_count = flatten_directory_by_traversal(root, None, &None, &None, false).unwrap(); assert_eq!(moved_count, 0); } + + // Tests for quiet mode + #[test] + fn test_flatten_quiet_mode_basic() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + + // Create subdirectory with files + let subdir = root.join("subdir"); + fs::create_dir(&subdir).unwrap(); + fs::write(subdir.join("test1.txt"), "content1").unwrap(); + fs::write(subdir.join("test2.txt"), "content2").unwrap(); + + // Test with quiet mode enabled + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, true).unwrap(); + + // Verify files were moved correctly despite quiet mode + assert_eq!(moved_count, 2); + assert!(root.join("test1.txt").exists()); + assert!(root.join("test2.txt").exists()); + assert_eq!( + fs::read_to_string(root.join("test1.txt")).unwrap(), + "content1" + ); + assert_eq!( + fs::read_to_string(root.join("test2.txt")).unwrap(), + "content2" + ); + } + + #[test] + fn test_flatten_quiet_mode_with_conflicts() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + + // Create a file in root + fs::write(root.join("test.txt"), "root content").unwrap(); + + // Create subdirectory with conflicting filename + let subdir = root.join("subdir"); + fs::create_dir(&subdir).unwrap(); + fs::write(subdir.join("test.txt"), "subdir content").unwrap(); + + // Test with quiet mode enabled + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, true).unwrap(); + + // Verify conflict resolution works in quiet mode + assert_eq!(moved_count, 1); + assert_eq!( + fs::read_to_string(root.join("test.txt")).unwrap(), + "root content" + ); + assert!(root.join("test_1.txt").exists()); + assert_eq!( + fs::read_to_string(root.join("test_1.txt")).unwrap(), + "subdir content" + ); + } + + #[test] + fn test_flatten_quiet_mode_with_depth() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + create_test_structure(root).unwrap(); + + // Test with quiet mode and max depth + let moved_count = flatten_directory_by_traversal(root, Some(2), &None, &None, true).unwrap(); + + // Verify depth limiting works in quiet mode + assert_eq!(moved_count, 2); + assert!(root.join("file1.txt").exists()); + assert!(root.join("file2.txt").exists()); + assert!(!root.join("file3.txt").exists()); + assert!(!root.join("file4.txt").exists()); + } + + #[test] + fn test_flatten_quiet_mode_with_include_filter() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + create_multi_dir_structure(root).unwrap(); + + let include = Some(vec!["src".to_string()]); + // Test with quiet mode and include filter + let moved_count = flatten_directory_by_traversal(root, None, &include, &None, true).unwrap(); + + // Verify filtering works in quiet mode + assert_eq!(moved_count, 1); + assert!(root.join("main.rs").exists()); + assert!(!root.join("readme.txt").exists()); + assert!(!root.join("test1.rs").exists()); + } + + #[test] + fn test_flatten_quiet_mode_with_exclude_filter() { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + create_multi_dir_structure(root).unwrap(); + + let exclude = Some(vec!["src".to_string()]); + // Test with quiet mode and exclude filter + let moved_count = flatten_directory_by_traversal(root, None, &None, &exclude, true).unwrap(); + + // Verify excluding works in quiet mode + assert_eq!(moved_count, 3); + assert!(!root.join("main.rs").exists()); + assert!(root.join("readme.txt").exists()); + assert!(root.join("test1.rs").exists()); + assert!(root.join("guide.txt").exists()); + } + + #[test] + fn test_flatten_quiet_vs_normal_same_result() { + // Verify that quiet mode produces the same file operations as normal mode + let temp_dir1 = TempDir::new().unwrap(); + let root1 = temp_dir1.path(); + + let temp_dir2 = TempDir::new().unwrap(); + let root2 = temp_dir2.path(); + + // Create identical structures + let subdir1 = root1.join("subdir"); + fs::create_dir(&subdir1).unwrap(); + fs::write(subdir1.join("file1.txt"), "content1").unwrap(); + fs::write(subdir1.join("file2.txt"), "content2").unwrap(); + + let subdir2 = root2.join("subdir"); + fs::create_dir(&subdir2).unwrap(); + fs::write(subdir2.join("file1.txt"), "content1").unwrap(); + fs::write(subdir2.join("file2.txt"), "content2").unwrap(); + + // Run with normal mode + let count1 = flatten_directory_by_traversal(root1, None, &None, &None, false).unwrap(); + + // Run with quiet mode + let count2 = flatten_directory_by_traversal(root2, None, &None, &None, true).unwrap(); + + // Verify same number of files moved + assert_eq!(count1, count2); + assert_eq!(count1, 2); + + // Verify same files exist in both directories + assert!(root1.join("file1.txt").exists()); + assert!(root1.join("file2.txt").exists()); + assert!(root2.join("file1.txt").exists()); + assert!(root2.join("file2.txt").exists()); + + // Verify same content + assert_eq!( + fs::read_to_string(root1.join("file1.txt")).unwrap(), + fs::read_to_string(root2.join("file1.txt")).unwrap() + ); + assert_eq!( + fs::read_to_string(root1.join("file2.txt")).unwrap(), + fs::read_to_string(root2.join("file2.txt")).unwrap() + ); + } } From f136970459f34c8de036e120c0b9e603495b9c5e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Nov 2025 05:44:41 +0000 Subject: [PATCH 3/4] Add test to verify errors are output in quiet mode Added test_flatten_quiet_mode_outputs_errors which verifies that error messages are still output to stderr even when quiet mode is enabled. The test creates a scenario where one file move fails (by creating a directory with the same name as the destination file) and another succeeds. It verifies: - The error causes the file move to fail (file remains in subdirectory) - The error message is written to stderr via eprintln! (unaffected by quiet mode) - Other file operations continue successfully - The moved file count reflects only successful moves This ensures quiet mode suppresses informational stdout but preserves error reporting via stderr. To manually verify stderr output, run: cargo test test_flatten_quiet_mode_outputs_errors -- --nocapture --- src/main.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/main.rs b/src/main.rs index 8ea3915..78c5ab4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -930,4 +930,52 @@ mod tests { fs::read_to_string(root2.join("file2.txt")).unwrap() ); } + + #[test] + fn test_flatten_quiet_mode_outputs_errors() { + // This test verifies that errors are still output even in quiet mode + // Quiet mode should suppress informational output but NOT error messages + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + + // Create a subdirectory with files + let subdir = root.join("subdir"); + fs::create_dir(&subdir).unwrap(); + fs::write(subdir.join("blocked.txt"), "will fail to move").unwrap(); + fs::write(subdir.join("success.txt"), "will move successfully").unwrap(); + + // Create a DIRECTORY (not a file) in root with the same name as one of the files + // This will cause fs::rename to fail for blocked.txt because you can't rename + // a file to a path that already exists as a directory + let blocking_dir = root.join("blocked.txt"); + fs::create_dir(&blocking_dir).unwrap(); + + // Run with quiet mode enabled + // The function should continue despite the error and return Ok + let moved_count = flatten_directory_by_traversal(root, None, &None, &None, true).unwrap(); + + // Verify only the successful file was moved (count should be 1, not 2) + assert_eq!(moved_count, 1); + + // Verify success.txt was moved successfully + assert!(root.join("success.txt").exists()); + assert_eq!( + fs::read_to_string(root.join("success.txt")).unwrap(), + "will move successfully" + ); + + // Verify blocked.txt was NOT moved (still in subdirectory) + assert!(subdir.join("blocked.txt").exists()); + + // Verify the blocking directory still exists + assert!(blocking_dir.exists()); + assert!(blocking_dir.is_dir()); + + // Note: This test verifies the error BEHAVIOR (file not moved, operation continues) + // The actual error message "Error moving..." is written to stderr via eprintln! + // In a real run with quiet mode, you would see: + // stderr: "Error moving /path/to/subdir/blocked.txt: ..." + // stdout: (empty - no "Moved:" messages due to quiet mode) + // To verify stderr output, run: cargo test test_flatten_quiet_mode_outputs_errors -- --nocapture + } } From 447304ba53a3a5f2a95e6a6827b10a070def5240 Mon Sep 17 00:00:00 2001 From: Andrew Nissen Date: Thu, 6 Nov 2025 12:49:34 -0500 Subject: [PATCH 4/4] updates --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 ++ src/main.rs | 6 ++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90837d0..7cdaa7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,7 +200,7 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rflatten" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 5a72344..1c55c03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rflatten" -version = "0.1.0" +version = "0.2.0" edition = "2024" description = "Flatten subdirectories by moving all files to the root directory" license = "GPL-3.0" diff --git a/README.md b/README.md index 0011425..9faa4cb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +`cargo install rflatten` + ``` RFLATTEN(1) User Commands RFLATTEN(1) diff --git a/src/main.rs b/src/main.rs index 78c5ab4..437a948 100644 --- a/src/main.rs +++ b/src/main.rs @@ -273,6 +273,12 @@ fn flatten_directory_by_traversal_recursive( // Handle filename conflicts by appending a number let mut counter = 1; while dest.exists() { + // If the destination exists but is a directory, don't try to rename + // Let fs::rename fail and handle the error below + if dest.is_dir() { + break; + } + let stem = Path::new(file_name) .file_stem() .and_then(|s| s.to_str())