fix: Versteckte Dateien (.gitignore etc.) werden nicht mehr fälschlicherweise umbenannt
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
227a1bd7b4
commit
010b5ad8af
4 changed files with 162 additions and 32 deletions
133
CLAUDE.md
Normal file
133
CLAUDE.md
Normal file
|
|
@ -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<String, String>` 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<Regex>` 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)
|
||||||
|
|
@ -135,10 +135,8 @@ impl Config {
|
||||||
config.replacements.len()
|
config.replacements.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else if verbose {
|
||||||
if verbose {
|
info!("Keine Konfigurationsdateien gefunden. Verwende nur die Standardwerte.");
|
||||||
info!("Keine Konfigurationsdateien gefunden. Verwende nur die Standardwerte.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
|
|
|
||||||
28
src/main.rs
28
src/main.rs
|
|
@ -82,13 +82,11 @@ fn main() -> Result<()> {
|
||||||
let old_path = entry.path();
|
let old_path = entry.path();
|
||||||
|
|
||||||
// Ebenentiefe 0 -> überspringen wir als Verzeichnis, außer --modify_root ist gesetzt
|
// Ebenentiefe 0 -> überspringen wir als Verzeichnis, außer --modify_root ist gesetzt
|
||||||
if entry.depth() == 0 {
|
if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root {
|
||||||
if entry.file_type().is_dir() && !args.modify_root {
|
if args.verbose {
|
||||||
if args.verbose {
|
debug!("Skip root directory: {}", old_path.display());
|
||||||
debug!("Skip root directory: {}", old_path.display());
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dateiname (oder Verzeichnisname) ermitteln
|
// Dateiname (oder Verzeichnisname) ermitteln
|
||||||
|
|
@ -107,16 +105,14 @@ fn main() -> Result<()> {
|
||||||
info!("{} -> {}", old_path.display(), new_path.display());
|
info!("{} -> {}", old_path.display(), new_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !args.no_changes {
|
if !args.no_changes && is_safe_rename(old_path, &new_path, args.force) {
|
||||||
if is_safe_rename(old_path, &new_path, args.force) {
|
fs::rename(old_path, &new_path).with_context(|| {
|
||||||
fs::rename(old_path, &new_path).with_context(|| {
|
format!(
|
||||||
format!(
|
"Fehler beim Umbenennen: {} -> {}",
|
||||||
"Fehler beim Umbenennen: {} -> {}",
|
old_path.display(),
|
||||||
old_path.display(),
|
new_path.display()
|
||||||
new_path.display()
|
)
|
||||||
)
|
})?;
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use emojis;
|
|
||||||
use glob::Pattern;
|
use glob::Pattern;
|
||||||
use log::{debug, warn};
|
use log::{debug, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
@ -18,10 +17,16 @@ static RE_MULTI: Lazy<Regex> = Lazy::new(|| Regex::new(r"[_\.]{2,}").unwrap());
|
||||||
pub fn clean_filename(name: &OsStr, config: &Config, verbose: bool) -> Option<String> {
|
pub fn clean_filename(name: &OsStr, config: &Config, verbose: bool) -> Option<String> {
|
||||||
let original = name.to_string_lossy();
|
let original = name.to_string_lossy();
|
||||||
|
|
||||||
// Stamm und Extension trennen
|
// Versteckte Dateien (mit führendem Punkt) korrekt behandeln
|
||||||
let (mut base, mut ext) = match original.rsplit_once('.') {
|
let (hidden_prefix, rest) = match original.strip_prefix('.') {
|
||||||
Some((b, e)) => (b.to_string(), format!(".{e}")),
|
Some(rest) => (".", rest),
|
||||||
None => (original.to_string(), String::new()),
|
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
|
// Platzhalter (C++, c++, C#, c#) anlegen
|
||||||
|
|
@ -71,8 +76,8 @@ pub fn clean_filename(name: &OsStr, config: &Config, verbose: bool) -> Option<St
|
||||||
base = "unnamed".to_string();
|
base = "unnamed".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endgültigen Dateinamen zusammenbauen
|
// Endgültigen Dateinamen zusammenbauen (hidden_prefix wieder hinzufügen)
|
||||||
let mut result = format!("{}{}", base, ext);
|
let mut result = format!("{}{}{}", hidden_prefix, base, ext);
|
||||||
|
|
||||||
// Platzhalter zurückverwandeln
|
// Platzhalter zurückverwandeln
|
||||||
result = restore_special_identifiers(&result);
|
result = restore_special_identifiers(&result);
|
||||||
|
|
@ -146,9 +151,7 @@ fn replace_emojis_and_superscript(input: &str) -> String {
|
||||||
input
|
input
|
||||||
.graphemes(true)
|
.graphemes(true)
|
||||||
.map(|g| {
|
.map(|g| {
|
||||||
if emojis::get(g).is_some() {
|
if emojis::get(g).is_some() || is_superscript(g) {
|
||||||
"_".to_string()
|
|
||||||
} else if is_superscript(g) {
|
|
||||||
"_".to_string()
|
"_".to_string()
|
||||||
} else {
|
} else {
|
||||||
g.to_string()
|
g.to_string()
|
||||||
|
|
@ -162,9 +165,9 @@ fn is_superscript(g: &str) -> bool {
|
||||||
g.chars().all(|c| {
|
g.chars().all(|c| {
|
||||||
c == '\u{00AA}'
|
c == '\u{00AA}'
|
||||||
|| c == '\u{00BA}'
|
|| c == '\u{00BA}'
|
||||||
|| (c >= '\u{00B2}' && c <= '\u{00B3}')
|
|| ('\u{00B2}'..='\u{00B3}').contains(&c)
|
||||||
|| c == '\u{00B9}'
|
|| c == '\u{00B9}'
|
||||||
|| (c >= '\u{2070}' && c <= '\u{209F}')
|
|| ('\u{2070}'..='\u{209F}').contains(&c)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue