From df4421af2ffbfb6898f0f62bba2e864596309077 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 10 Feb 2026 09:07:30 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Versteckte=20Dateien=20(.gitignore=20etc?= =?UTF-8?q?.)=20werden=20nicht=20mehr=20f=C3=A4lschlicherweise=20umbenannt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bugfix: Dateien mit führendem Punkt wurden zu "unnamed.xxx" umbenannt, da der Punkt fälschlicherweise als Extension-Trenner interpretiert wurde. Jetzt wird der führende Punkt als hidden_prefix separat behandelt. - Alle Clippy-Warnungen behoben (redundanter Import, kollabierbare if-Blöcke, manuelle Range-Checks) - CLAUDE.md für Projekt-Dokumentation hinzugefügt Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 6 +-- src/main.rs | 28 +++++----- src/sanitizer.rs | 27 +++++----- 4 files changed, 162 insertions(+), 32 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a3182b7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,133 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Projekt-Übersicht + +**NameToUnix** ist ein Rust-CLI-Tool zum Bereinigen von Datei- und Verzeichnisnamen gemäß Linux-Konventionen. Es ersetzt Leerzeichen, Sonderzeichen und deutsche Umlaute und arbeitet rekursiv durch Verzeichnisbäume. + +Dies ist das erste Rust-Projekt des Autors - Code-Verbesserungen sollten klar und didaktisch begründet werden. + +## Build & Test + +```bash +# Development build +cargo build + +# Release build (optimiert, für Distribution) +cargo build --release + +# Binary liegt dann unter: +# target/release/NameToUnix + +# Tests ausführen +cargo test + +# Tests mit Ausgabe +cargo test -- --nocapture + +# Linting +cargo clippy + +# Formatierung prüfen +cargo fmt --check + +# Formatierung anwenden +cargo fmt +``` + +## Lokales Testen + +```bash +# Test-Verzeichnisbaum mit skurrilen Dateinamen erstellen +cd test +./create_test_tree.sh + +# Vorschau (keine Änderungen): +cargo run -- -n ./testverzeichnis + +# Mit Änderungen: +cargo run -- ./testverzeichnis + +# Mit Verbose-Ausgabe: +cargo run -- -v ./testverzeichnis +``` + +## Code-Architektur + +Das Projekt ist in Module aufgeteilt: + +- **`main.rs`**: Entry Point + - Initialisiert Logger (`env_logger`) + - Parsed CLI-Argumente mit `clap` + - Lädt Konfiguration aus bis zu 3 Orten (Prioritätsreihenfolge: lokal > user > global) + - Sammelt alle Datei-/Verzeichniseinträge via `walkdir` + - Sortiert nach Tiefe (tiefste zuerst), um Parent-Verzeichnisse nicht zu früh umzubenennen + - Zeigt optional Fortschrittsbalken (`indicatif`) bei >50 Einträgen + +- **`cli.rs`**: Command-Line Interface + - Definiert `Cli`-Struct mit `clap::Parser` + - Argumente: `paths`, `--quiet`, `--no-changes`, `--force`, `--exclude`, `--verbose`, `--modify-root` + - `ArgGroup` verhindert gleichzeitige Nutzung von `--no-changes` und `--force` + +- **`config.rs`**: Konfigurationsmanagement + - `Config`-Struct mit `HashMap` für benutzerdefinierte Ersetzungen + - Lädt TOML-Dateien aus 3 Orten (in dieser Reihenfolge): + 1. `/etc/NameToUnix/config.toml` (System-global) + 2. `~/.config/NameToUnix/config.toml` (User-spezifisch) + 3. `./.NameToUnix.conf` (Arbeitsverzeichnis, höchste Priorität) + - Spätere Configs überschreiben frühere (`extend`) + +- **`sanitizer.rs`**: Kern-Logik der Dateinamen-Bereinigung + - `clean_filename()`: Hauptfunktion + - Trennt Stamm und Extension + - Schützt spezielle Identifikatoren (`C++`, `C#`) via Platzhalter + - Wendet zuerst Config-Replacements an, dann hardcoded Replacements + - Ersetzt Emojis und hochgestellte Zeichen durch `_` + - Entfernt ungültige Zeichen via Regex (`[^\w.\-]`) + - Normalisiert Punkt/Unterstrich-Kombinationen + - Trimmt führende Unterstriche (behält aber führenden Punkt) + - Stellt Platzhalter wieder her + - `is_excluded()`: Prüft Glob-Patterns + - `is_safe_rename()`: Sicherheitscheck vor Umbenennung (prüft ob Ziel existiert ohne `--force`) + +## Konfigurationsdatei-Logik + +- TOML-Format mit Sektion `[replacements]` +- **Wichtig**: Hardcoded-Transformationen (z.B. Apostroph-Entfernung, Umlaut-Ersetzung) sind immer vorrangig und nicht deaktivierbar +- Config-Replacements werden **vor** den hardcoded Replacements angewendet +- Lokale Configs überschreiben User-Configs überschreiben System-Configs + +## Besonderheiten + +1. **Tiefenbasierte Sortierung**: Umbenennung erfolgt von den tiefsten Verzeichnissen/Dateien nach oben, um Konflikte zu vermeiden (Parent-Verzeichnisse dürfen nicht vor ihren Kindern umbenannt werden). + +2. **Platzhalter für Spezialfälle**: `C++`, `c++`, `C#`, `c#` werden temporär durch Token (`CPLUSPLUS`, etc.) geschützt, damit `+` und `#` nicht durch Unterstriche ersetzt werden. + +3. **Emoji-Erkennung**: Nutzt das `emojis`-Crate zur Emoji-Erkennung und Unicode-Segmentation für grapheme-korrekte Verarbeitung. + +4. **Performance-Optimierung**: Regex-Patterns sind als `static Lazy` definiert (via `once_cell`) für wiederholte Nutzung ohne Re-Compilation. + +5. **Root-Directory-Schutz**: Standardmäßig wird das Root-Verzeichnis (depth 0) nicht umbenannt, außer `--modify-root` ist gesetzt. + +## Verwendete Dependencies + +- `clap` mit derive-Feature für CLI-Parsing +- `walkdir` für rekursives Traversieren +- `glob` für Exclude-Pattern +- `serde` + `toml` für Config-Dateien +- `anyhow` für Error-Handling +- `log` + `env_logger` für Logging +- `indicatif` für Progress-Bars +- `regex` für Pattern-Matching +- `unicode-segmentation` für korrekte grapheme-basierte String-Verarbeitung +- `emojis` für Emoji-Erkennung +- `once_cell` für lazy-static Regex-Patterns +- `dirs` zum Finden des User-Home-Verzeichnisses + +## Wichtige Hinweise für Code-Änderungen + +- Die Reihenfolge der Transformationen in `clean_filename()` ist wichtig und sollte nicht ohne Tests geändert werden +- Hardcoded Replacements sind bewusst nicht über Config deaktivierbar (Design-Entscheidung) +- Tests sollten mit dem `create_test_tree.sh`-Skript durchgeführt werden +- Bei Regex-Änderungen: Performance-Impact beachten (werden für jede Datei ausgeführt) diff --git a/src/config.rs b/src/config.rs index 08c124e..6ab6534 100644 --- a/src/config.rs +++ b/src/config.rs @@ -135,10 +135,8 @@ impl Config { config.replacements.len() ); } - } else { - if verbose { - info!("Keine Konfigurationsdateien gefunden. Verwende nur die Standardwerte."); - } + } else if verbose { + info!("Keine Konfigurationsdateien gefunden. Verwende nur die Standardwerte."); } Ok(config) diff --git a/src/main.rs b/src/main.rs index 91f5bcc..8907bfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,13 +82,11 @@ fn main() -> Result<()> { let old_path = entry.path(); // Ebenentiefe 0 -> überspringen wir als Verzeichnis, außer --modify_root ist gesetzt - if entry.depth() == 0 { - if entry.file_type().is_dir() && !args.modify_root { - if args.verbose { - debug!("Skip root directory: {}", old_path.display()); - } - continue; + if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root { + if args.verbose { + debug!("Skip root directory: {}", old_path.display()); } + continue; } // Dateiname (oder Verzeichnisname) ermitteln @@ -107,16 +105,14 @@ fn main() -> Result<()> { info!("{} -> {}", old_path.display(), new_path.display()); } - if !args.no_changes { - if is_safe_rename(old_path, &new_path, args.force) { - fs::rename(old_path, &new_path).with_context(|| { - format!( - "Fehler beim Umbenennen: {} -> {}", - old_path.display(), - new_path.display() - ) - })?; - } + if !args.no_changes && is_safe_rename(old_path, &new_path, args.force) { + fs::rename(old_path, &new_path).with_context(|| { + format!( + "Fehler beim Umbenennen: {} -> {}", + old_path.display(), + new_path.display() + ) + })?; } } } diff --git a/src/sanitizer.rs b/src/sanitizer.rs index ff65696..c0badfb 100644 --- a/src/sanitizer.rs +++ b/src/sanitizer.rs @@ -1,5 +1,4 @@ use crate::config::Config; -use emojis; use glob::Pattern; use log::{debug, warn}; use once_cell::sync::Lazy; @@ -18,10 +17,16 @@ static RE_MULTI: Lazy = Lazy::new(|| Regex::new(r"[_\.]{2,}").unwrap()); pub fn clean_filename(name: &OsStr, config: &Config, verbose: bool) -> Option { let original = name.to_string_lossy(); - // Stamm und Extension trennen - let (mut base, mut ext) = match original.rsplit_once('.') { - Some((b, e)) => (b.to_string(), format!(".{e}")), - None => (original.to_string(), String::new()), + // Versteckte Dateien (mit führendem Punkt) korrekt behandeln + let (hidden_prefix, rest) = match original.strip_prefix('.') { + Some(rest) => (".", rest), + None => ("", original.as_ref()), + }; + + // Stamm und Extension trennen (nur im Rest, nicht im hidden_prefix) + let (mut base, mut ext) = match rest.rsplit_once('.') { + Some((b, e)) if !b.is_empty() => (b.to_string(), format!(".{e}")), + _ => (rest.to_string(), String::new()), }; // Platzhalter (C++, c++, C#, c#) anlegen @@ -71,8 +76,8 @@ pub fn clean_filename(name: &OsStr, config: &Config, verbose: bool) -> Option String { input .graphemes(true) .map(|g| { - if emojis::get(g).is_some() { - "_".to_string() - } else if is_superscript(g) { + if emojis::get(g).is_some() || is_superscript(g) { "_".to_string() } else { g.to_string() @@ -162,9 +165,9 @@ fn is_superscript(g: &str) -> bool { g.chars().all(|c| { c == '\u{00AA}' || c == '\u{00BA}' - || (c >= '\u{00B2}' && c <= '\u{00B3}') + || ('\u{00B2}'..='\u{00B3}').contains(&c) || c == '\u{00B9}' - || (c >= '\u{2070}' && c <= '\u{209F}') + || ('\u{2070}'..='\u{209F}').contains(&c) }) }