feat: Add --max-depth option and safe symlink handling (v1.2.0)

## Neue Features

- **--max-depth N**: Begrenzt Rekursionstiefe auf N Ebenen (erfordert -r)
  - Nützlich für sehr tiefe Verzeichnisbäume (z.B. node_modules)
  - Verhindert unnötige Traversierung tiefer Strukturen

- **Explizites Symlink-Handling**:
  - Standard: Symlinks werden komplett übersprungen (sicher)
  - Mit --special: Nur Symlink-Namen werden bereinigt, Ziel bleibt unangetastet
  - follow_links(false) explizit gesetzt zur Vermeidung von Endlosschleifen
  - Verhindert unbeabsichtigte Änderungen außerhalb des Zielverzeichnisses

- **Verbose Symlink-Logging**: Zeigt mit -v welche Symlinks übersprungen werden

## Tests

- 5 neue Integration-Tests hinzugefügt:
  - test_max_depth_option
  - test_max_depth_requires_recursive
  - test_symlinks_default_behavior (Unix only)
  - test_symlinks_with_special_flag (Unix only)
  - test_symlinks_not_followed (Unix only)

- Alle 30 Tests bestehen (25 bestehende + 5 neue)

## Dokumentation

- README.md: Neue Beispiele und "Symlink Behavior" Sektion
- CHANGELOG.md: v1.2.0 Eintrag mit allen Änderungen
- man/ntu.1: --max-depth Option und SYMLINK BEHAVIOR Sektion
- CLAUDE.md: Aktualisierte Code-Architektur Dokumentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-02-12 13:39:41 +01:00
commit b84dd70f80
8 changed files with 228 additions and 7 deletions

View file

@ -21,6 +21,10 @@ pub struct Cli {
#[clap(short = 'r', long)]
pub recursive: bool,
/// Maximale Rekursionstiefe (erfordert --recursive)
#[clap(long, value_name = "N", requires = "recursive")]
pub max_depth: Option<usize>,
/// Wählt eine Transformations-Sequenz aus (default, lower, upper, minimal, utf-8)
#[clap(short = 's', long, value_name = "NAME")]
pub sequence: Option<String>,

View file

@ -118,11 +118,18 @@ fn main() -> Result<()> {
let mut entries = Vec::new();
let walker = if args.recursive {
// Recursive: unbegrenzte Tiefe
WalkDir::new(path)
// Recursive: mit optionaler max_depth
let mut w = WalkDir::new(path).follow_links(false); // Symlinks nicht folgen
if let Some(depth) = args.max_depth {
w = w.max_depth(depth);
if args.verbose {
info!("Maximale Rekursionstiefe: {}", depth);
}
}
w
} else {
// Non-recursive: max_depth(1) verarbeitet nur direkte Kinder
WalkDir::new(path).max_depth(1)
WalkDir::new(path).max_depth(1).follow_links(false)
};
for entry_result in walker
@ -164,9 +171,12 @@ fn main() -> Result<()> {
return None;
}
// Special Files nur mit --special
// Special Files (Symlinks, Sockets, etc.) nur mit --special
let file_type = entry.file_type();
if !args.special && (!file_type.is_file() && !file_type.is_dir()) {
if args.verbose && file_type.is_symlink() {
debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display());
}
return None;
}
@ -194,6 +204,9 @@ fn main() -> Result<()> {
let file_type = entry.file_type();
if !args.special && (!file_type.is_file() && !file_type.is_dir()) {
if args.verbose && file_type.is_symlink() {
debug!("Überspringe Symlink: {} (nutze --special um Symlink-Namen zu bereinigen)", old_path.display());
}
return None;
}