Compare commits

...

No commits in common. "5e272f438702fd2fddb01e082efa9a03dbff4566" and "0ba2057514e186e51274bec0e5ed74e7de9b1d93" have entirely different histories.

583 changed files with 6443 additions and 24 deletions

28
.NameToUnix.conf Normal file
View file

@ -0,0 +1,28 @@
# .NameToUnix.conf (TOML)
# --------------------------------------------
# In dieser Datei können beliebige zusätzliche Schlüssel-Werte-Paare unter [replacements] hinterlegt werden,
# die im Dateistammnamen ersetzt werden. Zum Beispiel:
#
# [replacements]
# "foo" = "bar"
# "old" = "neu"
#
# Dadurch werden in den Dateinamen alle "foo" durch "bar" ersetzt, und "old" durch "neu".
# WICHTIG: Die hartcodierten Transformationen sind aber immer vorrangig und lassen sich auch nicht rückgängig machen.
# Weitere Einstellungen können analog ergänzt werden, wenn man das Struct "Config" erweitert.
[replacements]
".." = "."
"_·_" = "_-_"
".-_" = "_-_"
"Ä" = "Ae"
"Ö" = "Oe"
"Ü" = "Ue"
"ä" = "ae"
"ö" = "oe"
"ü" = "ue"
"ß" = "ss"
"O'Reilly" = "OReilly"
"O_Reilly" = "OReilly"
# "" = ""

107
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,107 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
- beta
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run tests
run: cargo test --verbose
- name: Run tests (release mode)
run: cargo test --release --verbose
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check formatting
run: cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Run clippy
run: cargo clippy --all-targets --all-features -- -D warnings
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-latest
target: x86_64-apple-darwin
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
target: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ntu-${{ matrix.target }}
path: target/${{ matrix.target }}/release/ntu

66
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
name: Release
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: ntu
asset_name: ntu-linux-x86_64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
artifact_name: ntu
asset_name: ntu-linux-x86_64-musl
- os: macos-latest
target: x86_64-apple-darwin
artifact_name: ntu
asset_name: ntu-macos-x86_64
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: ntu
asset_name: ntu-macos-arm64
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
target: ${{ matrix.target }}
- name: Install musl-tools (Linux musl only)
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Strip binary (Linux only)
if: startsWith(matrix.os, 'ubuntu')
run: strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
- name: Compress binary
run: |
cd target/${{ matrix.target }}/release
tar czf ${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }}
mv ${{ matrix.asset_name }}.tar.gz ../../..
- name: Upload binary to release
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.asset_name }}.tar.gz
body_path: CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

45
.gitignore vendored
View file

@ -1,22 +1,29 @@
# ---> Rust # Rust / Cargo
# Generated by Cargo /target
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb
# RustRover # IDEs
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can .idea/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore .vscode/
# and can be added to the global gitignore or merged into this file. For a more nuclear *.swp
# option (not recommended) you can uncomment the following to ignore the entire idea folder. *.swo
#.idea/ *~
# OS
.DS_Store
Thumbs.db
# Project-specific
info/
CLAUDE.md
# Test artifacts
/test/testverzeichnis
# Temporary files
*.tmp
*.log
*.bak

120
CHANGELOG.md Normal file
View file

@ -0,0 +1,120 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.0] - 2025-02-10
### Added
- **`-s/--sequence <name>` option**: Select transformation sequence (default, lower, upper, minimal, utf-8)
- **`-L` option**: List all available sequences (use with `-v` for details)
- **5 hardcoded sequences**:
- `default`: Current behavior (umlauts→ASCII, spaces→underscores)
- `lower`: Like default + convert to lowercase
- `upper`: Like default + convert to UPPERCASE
- `minimal`: Only replace spaces, keep UTF-8 characters
- `utf-8`: UTF-8 friendly (keep umlauts, remove special chars)
### Changed
- Refactored `clean_filename()` to support sequence-based transformations
- Umlaut replacements moved from hardcoded to sequence-specific logic
- Case transformations now also apply to file extensions
### Technical
- Added `Sequence` struct and `CaseTransform` enum in `sanitizer.rs`
- Extended CLI with `-s` and `-L` options
- Added `list_sequences()` function in `main.rs`
- Updated all unit tests to pass `Sequence` parameter
- Added 8 new integration tests for sequence functionality
## [1.0.0] - 2025-02-10
### ⚠️ BREAKING CHANGES
- **Recursion is now opt-in**: By default, `ntu` only processes the specified
paths and their immediate children. Use `-r`/`--recursive` flag to enable
recursive directory traversal.
- Migration: Add `-r` to existing commands to preserve v0.x behavior
### Added
- **`--conf <FILE>` option**: Specify a single configuration file, bypassing
the default hierarchy (/etc → ~/.config → ./). Errors if file doesn't exist.
- **`-r/--recursive` flag**: Enable recursive directory processing
### Changed
- Default behavior is now non-recursive (use `-r` for recursive processing)
- All integration tests updated to explicitly use `-r` flag
### Migration Guide
```bash
# Old command (v0.x - always recursive):
ntu /path/to/files
# New equivalent (v1.x - explicit recursion):
ntu -r /path/to/files
```
## [0.3.0] - 2025-02-10
### Added
- **CI/CD Pipeline**: Automated testing and release builds via GitHub Actions
- CI workflow: Tests on Rust stable and beta, Clippy, rustfmt checks
- Release workflow: Multi-platform builds (Linux x86_64, Linux musl, macOS Intel, macOS ARM)
- **Shell Completions**: Auto-completion support for all major shells
- Bash completion (`completions/ntu.bash`)
- Zsh completion (`completions/_ntu`)
- Fish completion (`completions/ntu.fish`)
- **Manpage**: Professional manual page (`man/ntu.1`) with full documentation
- **Colored Output**: Terminal colors for better visual feedback
- Green for successful renames
- Yellow for dry-run mode
- Red for errors
- Cyan/bold for statistics
- `--no-color` flag to disable colors
- **Integration Tests**: 13 comprehensive integration tests using `assert_cmd`
- **README**: Installation instructions for pre-built binaries, badges (CI, Release, Version, License)
### Changed
- Improved `.gitignore` with better organization
- Better error messages with colored output
- Updated test framework to use modern `cargo_bin!` macro
### Fixed
- Removed unused `warn` import
## [0.2.0] - 2025-02-10
### Added
- **CLI**: `--dry-run` as primary option (with `--no-changes` as deprecated alias for backward compatibility)
- **CLI**: `--special` flag to process symlinks and special files (normally skipped)
- **Smart Default Excludes**: Automatically ignore `.git`, `.svn`, `node_modules`, `.cache`, `__pycache__`
- **Double Extensions**: Proper handling of `.tar.gz`, `.tar.bz2`, `.tar.xz`, `.tar.zst`, `.tar.lz`, `.tar.Z`
- **Parallel Processing**: Using `rayon` for parallel filename cleaning when processing ≥100 files
- **Write Permission Checks**: Check write permissions before attempting rename operations
- **Unit Tests**: 9 comprehensive tests for `clean_filename()` covering edge cases
### Fixed
- **Critical Bug**: Hidden files (like `.gitignore`) are no longer incorrectly renamed to `unnamed.xxx`
- Leading dot in hidden files is now correctly preserved
- Fixed all clippy warnings
### Changed
- Binary renamed from `NameToUnix` to `ntu` (shorter CLI usage)
- Improved error messages for permission issues
- Better handling of hidden files with spaces (`.my config``.my_config`)
### Performance
- Parallel processing with rayon for large directory trees (threshold: 100 files)
- Optimized regex patterns using `once_cell::Lazy`
## [0.1.0] - 2025-03-07
### Added
- Initial release
- Basic filename sanitization
- Configurable replacements via TOML
- Recursive directory processing
- Exclude patterns support
- German umlaut conversion
- Special identifier preservation (C++, C#)

136
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,136 @@
# Contributing to CONTRIBUTING.md
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## Code of Conduct
This project and everyone participating in it is governed by the
[CONTRIBUTING.md Code of Conduct](blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to <eliza@linix.de>.
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation]().
Before you ask a question, it is best to search for existing [Issues](/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Issue](/issues/new).
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
We will then take care of the issue as soon as possible.
## I Want To Contribute
> ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How Do I Submit a Good Bug Report?
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for CONTRIBUTING.md, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
- **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
### Your First Code Contribution
### Improving The Documentation
## Styleguides
### Commit Messages
## Join The Project Team
## Attribution
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!

1142
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

70
Cargo.toml Normal file
View file

@ -0,0 +1,70 @@
[package]
name = "NameToUnix"
version = "1.1.0"
edition = "2021"
authors = ["Dieter Schlüter <dieter.schlueter@linix.de>"]
description = "Ein Tool zum Anpassen von Verzeichnis- und Dateinamen an Linux-Konventionen"
license = "MIT"
readme = "README.md"
repository = "https://github.com/jamulix/NameToUnix"
keywords = ["filesystem", "rename", "sanitize", "cli"]
categories = ["command-line-utilities", "filesystem"]
[[bin]]
name = "ntu"
path = "src/main.rs"
[dependencies]
# Bereits verwendete Abhängigkeiten
clap = { version = "4.5.27", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8.11"
walkdir = "2.4.0"
glob = "0.3.1"
unicode-segmentation = "1.10.0"
emojis = "0.6.1"
regex = "1.10.3"
dirs = "5.0.1"
# Neue empfohlene Abhängigkeiten
anyhow = "1.0.80" # Verbesserte Fehlerbehandlung
thiserror = "1.0.57" # Typisierte Fehler
once_cell = "1.19.0" # Lazy-Initialisierung für statische Werte
rayon = "1.9.0" # Parallelverarbeitung
indicatif = "0.17.7" # Fortschrittsbalken
env_logger = "0.11.2" # Logging-Framework
log = "0.4.21" # Logging-Abstraktionen
itertools = "0.12.1" # Erweiterte Iterator-Funktionalität
colored = "2.1" # Farbige Terminal-Ausgabe
[dev-dependencies]
tempfile = "3.10.1" # Temporäre Dateien für Tests
assert_fs = "1.1.1" # Dateisystem-Assertions für Tests
predicates = "3.1.0" # Prädikate für Tests
assert_cmd = "2.0" # Command-Line Testing
[profile.release]
lto = true # Link-Time-Optimierung
codegen-units = 1 # Optimierung für Binärgröße
opt-level = 3 # Maximale Optimierung
panic = "abort" # Kleinere Binärdatei durch Abbrechen bei Panic
strip = true # Entfernen von Debug-Symbolen
[package.metadata.deb]
maintainer = "Dieter Schlüter <dieter.schlueter@linix.de>"
copyright = "2025, Dieter Schlüter"
license-file = ["LICENSE", "4"]
extended-description = """
NameToUnix ist ein Kommandozeilen-Tool zum Umbenennen von Dateien und Verzeichnissen,
um sie mit Linux-Dateinamen-Konventionen kompatibel zu machen. Es ersetzt Leerzeichen und
Sonderzeichen durch Unterstriche und konvertiert deutsche Umlaute in ihre ASCII-Pendants.
"""
depends = "$auto"
section = "utils"
priority = "optional"
assets = [
["target/release/ntu", "usr/bin/", "755"],
["README.md", "usr/share/doc/NameToUnix/README", "644"],
["man/ntu.1", "usr/share/man/man1/", "644"],
]

21
LICENSE
View file

@ -1,9 +1,22 @@
MIT License MIT License
Copyright (c) 2026 dschlueter Copyright (c) 2025 Dieter Schlüter
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

388
README.md
View file

@ -1,3 +1,387 @@
# ntu # NameToUnix
[![CI](https://github.com/jamulix/NameToUnix/workflows/CI/badge.svg)](https://github.com/jamulix/NameToUnix/actions)
[![Release](https://github.com/jamulix/NameToUnix/workflows/Release/badge.svg)](https://github.com/jamulix/NameToUnix/releases)
[![Version](https://img.shields.io/github/v/release/jamulix/NameToUnix)](https://github.com/jamulix/NameToUnix/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
**Filename Repair Tool for Linux** · Binary: `ntu`
(german and english)
A powerful command line tool for cleaning up file names according to Linux conventions.
It works under Linux. The program is useful if many file names, e.g. after downloading and unpacking zip files from Windows file systems
contain spaces or special characters. It saves an enormous amount of time by automatically replacing the offending characters.
I have been using a similar program - a Perl script - for about 15 years. It has saved me many, many hours of mindless renaming work. Now I'm learning Rust and wanted to write a useful command line application. ***NameToUnix*** is the result.
This is my first program in Rust. (Please have mercy on me.)
Ein leistungsstarkes Kommandozeilen-Tool zum Bereinigen von Dateinamen gemäß Linux-Konventionen.
Es funktioniert unter Linux. Das Programm ist sinnvoll, wenn viele Dateinamen z. B. nach einem Download und Entpacken von Zip-Dateien aus Windows-Dateisystemen
Leerzeichen oder Sonderzeichen enthalten. Es erspart enorm viel Zeit durch automatisches Ersetzen der störenden Zeichen.
Ich benutze ein ähnliches Programm - ein Perl-Skript - seit ca. 15 Jahren. Es hat mir schon viele, viele Stunden stumpfsinniger Umbenennungs-Arbeit erspart. Nun bin ich dabei, Rust zu lernen und wollte eine sinnvolle Kommandozeilenanwendung schreiben. ***NameToUnix*** ist dabei herausgekommen.
Dies ist mein erstes Programm in Rust. (Bitte seid gnädig.)
(c) 2025 Dieter Schlüter <dieter.schlueter@linix.de>
## ⚠️ BREAKING CHANGE in v1.0.0
**Recursion is now opt-in**: As of version 1.0.0, `ntu` processes only the
specified paths and their immediate children by default. Use the `-r` or
`--recursive` flag to enable recursive directory traversal.
**Migration**: Add `-r` to your existing commands:
```bash
# Old (v0.x): ntu /path/to/files
# New (v1.x): ntu -r /path/to/files
```
## Sequences
Starting with v1.1.0, `ntu` supports transformation sequences similar to detox. Sequences control how filenames are transformed:
- **default**: Standard transformation (umlauts→ASCII, spaces→underscores, remove special chars)
- **lower**: Like default, but convert to lowercase
- **upper**: Like default, but convert to UPPERCASE
- **minimal**: Only replace spaces, keep UTF-8 characters
- **utf-8**: UTF-8 friendly (keep umlauts, replace spaces, remove special chars)
```bash
# Use specific sequence
ntu -s lower /path/to/files
# List all available sequences
ntu -L
# List sequences with details
ntu -L -v
```
## Functions / Funktionen
- Replaces spaces and special characters in file and directory names with underscores
- Converts German umlauts to their ASCII counterparts (ä → ae, etc.)
- Supports recursive processing of directories
- Provides preview mode without actual changes
- Allows user-defined replacement rules via configuration file
- Supports exclusion patterns for specific file patterns/directory patterns
---
- Ersetzt Leerzeichen und Sonderzeichen in Datei- und Verzeichnisnamen durch Unterstriche
- Konvertiert deutsche Umlaute in ihre ASCII-Pendants (ä → ae, usw.)
- Unterstützt rekursive Verarbeitung von Verzeichnissen
- Bietet Vorschau-Modus ohne tatsächliche Änderungen
- Ermöglicht benutzerdefinierte Ersetzungsregeln über Konfigurationsdatei
- Unterstützt Ausschlussmuster für bestimmte Datei-Muster/Verzeichnis-Muster
## Installation
### Option 1: Pre-built Binary (Recommended)
Download the latest release for your platform from [GitHub Releases](https://github.com/jamulix/NameToUnix/releases):
```bash
# Linux x86_64
wget https://github.com/jamulix/NameToUnix/releases/latest/download/ntu-linux-x86_64.tar.gz
tar xzf ntu-linux-x86_64.tar.gz
sudo mv ntu /usr/local/bin/
# macOS Intel
wget https://github.com/jamulix/NameToUnix/releases/latest/download/ntu-macos-x86_64.tar.gz
tar xzf ntu-macos-x86_64.tar.gz
sudo mv ntu /usr/local/bin/
# macOS Apple Silicon (M1/M2)
wget https://github.com/jamulix/NameToUnix/releases/latest/download/ntu-macos-arm64.tar.gz
tar xzf ntu-macos-arm64.tar.gz
sudo mv ntu /usr/local/bin/
```
### Option 2: Build from Source
Die ausführbare Datei wird unter `target/release/ntu` erstellt. Du solltest sie mit 'sudo cp target/release/ntu /usr/local/bin/' kopieren. Sie ist dann für alle User verfügbar. Denke daran, die Konfiguationsdatei (s. u.) ebenfalls zu kopieren. Sie kann für jeden User individuell angepasst werden, wenn sie im home-Verzeichnis des Users liegt.
The executable file is created under `target/release/ntu`. You should copy it with 'sudo cp target/release/ntu /usr/local/bin/'. It is then available for all users. Remember to copy the configuration file (see below) as well. It can be customized for each user individually if it is located in the user's home directory.
```bash
git clone https://github.com/jamulix/NameToUnix.git # Download repository
cd NameToUnix # Change to download directory
cargo build --release # Build binary
sudo cp target/release/ntu /usr/local/bin/ # copy binary to local bin directory
# Globale Einstellungen / Global Settings
sudo mkdir -p /etc/NameToUnix/ # Create global config directory for NameToUnix in /etc
sudo cp .NameToUnix.conf /etc/NameToUnix/config.toml # Copy config file to this global directory
# Lokale Einstellungen / Local settings
mkdir -p ~/.config/NameToUnix/ # Create a personal config directory for NameToUnix
cp .NameToUnix.conf ~/.config/NameToUnix/config.toml # Copy config file to this personal directory
# Shell-Completion (optional)
sudo cp completions/ntu.bash /etc/bash_completion.d/ntu # Bash completion
# Oder für Zsh:
sudo cp completions/_ntu /usr/share/zsh/site-functions/_ntu # Zsh completion
# Oder für Fish:
sudo cp completions/ntu.fish /usr/share/fish/vendor_completions.d/ntu.fish # Fish completion
# Manpage (optional)
sudo cp man/ntu.1 /usr/share/man/man1/ # Install manual page
sudo mandb # Update man database
```
## Usage
```bash
# Basic usage (non-recursive: only immediate children)
ntu /path/to/files
# Recursive processing (process subdirectories)
ntu -r /path/to/files
# Dry-run: only preview the changes without actual renaming
ntu --dry-run -r /path/to/files
ntu -n -r /path/to/files # Short form
# Use specific config file
ntu --conf /path/to/custom.toml /path/to/files
# Process multiple paths
ntu -r /path1 /path2 /path3
# Exclude specific files
ntu -r -e "*.tmp" -e "backup_*" /path/to/files
# Process symlinks and special files (normally skipped)
ntu -r --special /path/to/files
# Increase verbosity
ntu -r -v /path/to/files
# Also rename the root directory
ntu -r --modify-root /path/to/files
# Combine options
ntu --dry-run -r -v --special /path/to/files
# Use lowercase sequence
ntu -r -s lower /path/to/files
# Minimal mode (only spaces, keep UTF-8)
ntu -s minimal /path/to/files
# UTF-8 friendly mode
ntu -s utf-8 /path/to/files
# List available sequences
ntu -L
# List sequences with details
ntu -L -v
```
**Note:** The following directories/files are automatically excluded:
`.git`, `.svn`, `node_modules`, `.cache`, `__pycache__`
## Verwendung
```bash
# Grundlegende Verwendung (nicht-rekursiv: nur direkte Kinder)
ntu /pfad/zu/dateien
# Rekursive Verarbeitung (Unterverzeichnisse verarbeiten)
ntu -r /pfad/zu/dateien
# Dry-run: Nur Vorschau der Änderungen ohne tatsächliche Umbenennung
ntu --dry-run -r /pfad/zu/dateien
ntu -n -r /pfad/zu/dateien # Kurzform
# Spezifische Config-Datei verwenden
ntu --conf /pfad/zu/custom.toml /pfad/zu/dateien
# Mehrere Pfade verarbeiten
ntu -r /pfad1 /pfad2 /pfad3
# Bestimmte Dateien ausschließen
ntu -r -e "*.tmp" -e "backup_*" /pfad/zu/dateien
# Symlinks und Special Files verarbeiten (normalerweise übersprungen)
ntu -r --special /pfad/zu/dateien
# Verbosity erhöhen
ntu -r -v /pfad/zu/dateien
# Auch das Wurzelverzeichnis umbenennen
ntu -r --modify-root /pfad/zu/dateien
# Optionen kombinieren
ntu --dry-run -r -v --special /pfad/zu/dateien
# Kleinbuchstaben-Sequenz verwenden
ntu -r -s lower /pfad/zu/dateien
# Minimal-Modus (nur Leerzeichen, UTF-8 behalten)
ntu -s minimal /pfad/zu/dateien
# UTF-8 freundlicher Modus
ntu -s utf-8 /pfad/zu/dateien
# Verfügbare Sequenzen auflisten
ntu -L
# Sequenzen mit Details auflisten
ntu -L -v
```
**Hinweis:** Die folgenden Verzeichnisse/Dateien werden automatisch ausgeschlossen:
`.git`, `.svn`, `node_modules`, `.cache`, `__pycache__`
## Configuration File / Konfiguration
Erstelle eine Datei `.NameToUnix.conf` im persönlichen Arbeitsverzeichnis z. B. mit folgendem Inhalt
(alternativ `~/.config/NameToUnix/config.toml`):
Create a file `.NameToUnix.conf` in your personal working directory, e.g. with the following content
(alternatively `~/.config/NameToUnix/config.toml`):
```toml
[replacements]
"foo" = "bar"
"old" = "new"
"alt" = "neu"
".." = "."
"_·_" = "_-_"
"Ä" = "Ae"
"Ö" = "Oe"
"Ü" = "Ue"
"ä" = "ae"
"ö" = "oe"
"ü" = "ue"
"ß" = "ss"
```
Dies ist eine Beispielkonfiguration. Du kannst Die Datei nach Belieben anpassen.
The above is an example configuration. You can customize the file as you wish.
Du solltest auch eine zentrale Konfigurationsdatei /etc/NameToUnix/config.toml im globalen Verzeichnis /etc erstellen (Beispiel):
You should also create a central configuration file /etc/NameToUnix/config.toml in the global directory /etc (example):
```toml
# /etc/NameToUnix/config.toml
# --------------------------------------------
# In dieser Datei können beliebige zusätzliche Schlüssel-Werte-Paare unter [replacements] hinterlegt werden,
# die im Dateistammnamen ersetzt werden. Zum Beispiel:
#
# [replacements]
# "foo" = "bar"
# "old" = "neu"
#
# Dadurch werden in den Dateinamen alle "foo" durch "bar" ersetzt, und "old" durch "neu".
# WICHTIG: Die hartcodierten Transformationen sind aber immer vorrangig und lassen sich auch nicht rückgängig machen.
# Die persönlichen Einstellungen überschreiben diese Einstellungen.
# VORSICHT! Zuerst mit 'ntu -n <path>' testen
# --------------------------------------------
# In this file, any additional key-value pairs can be stored under [replacements],
# which are replaced in the file master name. For example:
#
# [replacements]
# “foo” = “bar”
# “old” = “new”
#
# This replaces all “foo” with “bar” and “old” with “new” in the file names.
# IMPORTANT: The hard-coded transformations always have priority and cannot be undone.
# The personal settings overwrite these settings.
# BE CAREFUL! First test with 'ntu -n <path>' (Just display the changes)
[replacements]
".." = "."
"_·_" = "_-_"
".-_" = "_-_"
# "" = "" # bewirkt nichts / Dummy entry
```
### Verwendung von NameToUnix
Um die Verwendung von `ntu` zu verstehen, kannst du die folgende Hilfe ausgeben:
```text
ntu --help
```
Die Ausgabe sieht wie folgt aus:
```text
Ein Tool zum Anpassen von Verzeichnis- und Dateinamen an Linux-Konventionen
Usage: ntu [OPTIONS] [PATHS]...
Arguments:
[PATHS]... Pfade (Dateien und Verzeichnisse) zum rekursiven Anpassen
Options:
-q, --quiet Ausgaben unterdrücken (keine Umbenennungsinfos auf stdout)
-n, --dry-run Nur anzeigen, aber keine realen Änderungen vornehmen (dry-run)
-f, --force Existierende Dateien überschreiben
-e, --exclude <PATTERN> Zu ignorierende Muster (-e "*.py", mehrere können angegeben werden)
-v, --verbose Ausführliche Debug-Informationen
--modify-root Erlaubt, auch das Wurzelverzeichnis anzupassen
--special Auch symbolische Links und Special Files verarbeiten
-h, --help Print help
-V, --version Print version
```
### Usage of NameToUnix
```text
A tool for adapting directories and file names to Linux conventions
Usage: ntu [OPTIONS] [PATHS]...
Arguments:
[PATHS]... Paths (files and directories) for recursive customization
Options:
-q, --quiet Suppress output (no renaming info on stdout)
-n, --dry-run Only display, but do not make any real changes (dry-run)
-f, --force Overwrite existing files
-e, --exclude <PATTERN> Patterns to be ignored (-e "*.py", several can be specified)
-v, --verbose Detailed debug information
--modify-root Allows you to customize the root directory as well
--special Also process symbolic links and special files
-h, --help Print help
-V, --version Print version
```
## Test
Im Verzeichnis [***./test***](./test) gibt es ein bash-Skript [***create_test_tree.sh***](test/create_test_tree.sh), das lokal 21 Test-Verzeichnisse und 400 Dateien mit skurrilen Zufallsnamen erzeugt. Damit kannst Du ***NameToUnix*** ausprobieren:
***ntu -n ./testverzeichnis*** (nur Anzeige der Änderungen)
oder
***ntu ./testverzeichnis*** (Anzeige mit Umbenennen).
In the directory [***./test***](./test) there is a bash script [***create_test_tree.sh***](test/create_test_tree.sh), which locally creates 21 test directories and 400 files with bizarre random names. You can use this to try out ***NameToUnix***:
***ntu -n ./testverzeichnis*** (display changes only)
or
***ntu ./testverzeichnis*** (display with renaming).
## Lizenz / License
This project is licensed under the MIT license - see the [LICENSE](LICENSE) file for details.
Dieses Projekt steht unter der MIT-Lizenz - siehe die [LICENSE](LICENSE)-Datei für Details.
## Mitwirken / Contributions
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the pull request process.
Beiträge sind willkommen! Bitte lies [CONTRIBUTING.md](CONTRIBUTING.md) für Details zum Prozess für Pull Requests.
Dieses Rust-Programm ntu ersetzt Leerzeichen und andere obskure Zeichen in Dateinamen.

27
completions/_ntu Normal file
View file

@ -0,0 +1,27 @@
#compdef ntu
# Zsh completion for ntu (NameToUnix)
_ntu() {
local curcontext="$curcontext" state line
typeset -A opt_args
_arguments -C \
'(-r --recursive)'{-r,--recursive}'[Process directories recursively]' \
'(-s --sequence)'{-s,--sequence}'[Use transformation sequence]:sequence:(default lower upper minimal utf-8)' \
'-L[List available sequences]' \
'--conf[Use specific configuration file]:config file:_files' \
'(-n --dry-run --no-changes)'{-n,--dry-run,--no-changes}'[Only preview changes without renaming]' \
'(-q --quiet)'{-q,--quiet}'[Suppress output]' \
'(-f --force)'{-f,--force}'[Overwrite existing files]' \
'*'{-e,--exclude}'[Exclude pattern]:pattern:' \
'(-v --verbose)'{-v,--verbose}'[Verbose debug output]' \
'--modify-root[Also rename root directory]' \
'--special[Process symlinks and special files]' \
'--no-color[Disable colored output]' \
'(-h --help)'{-h,--help}'[Print help]' \
'(-V --version)'{-V,--version}'[Print version]' \
'*:path:_files'
}
_ntu "$@"

45
completions/ntu.bash Normal file
View file

@ -0,0 +1,45 @@
# Bash completion for ntu (NameToUnix)
_ntu_completion() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# All available options
opts="--recursive --sequence --conf --dry-run --no-changes --quiet --force --exclude --verbose --modify-root --special --no-color --help --version -r -s -L -n -q -f -e -v -h -V"
# Handle options that require arguments
case "${prev}" in
-s|--sequence)
# Suggest available sequences
COMPREPLY=( $(compgen -W "default lower upper minimal utf-8" -- ${cur}) )
return 0
;;
-e|--exclude)
# Suggest glob patterns
COMPREPLY=( $(compgen -W '"*.tmp" "*.log" "*.bak" "*.swp" "*~"' -- ${cur}) )
return 0
;;
--conf)
# Suggest files for config option
COMPREPLY=( $(compgen -f -- ${cur}) )
return 0
;;
*)
;;
esac
# If current word starts with -, complete with options
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
# Otherwise complete with directories and files
COMPREPLY=( $(compgen -f -- ${cur}) )
return 0
}
# Register completion function
complete -F _ntu_completion ntu

24
completions/ntu.fish Normal file
View file

@ -0,0 +1,24 @@
# Fish completion for ntu (NameToUnix)
# Main command
complete -c ntu -f -d 'Sanitize file and directory names to Unix conventions'
# Options
complete -c ntu -s r -l recursive -d 'Process directories recursively'
complete -c ntu -s s -l sequence -d 'Use transformation sequence' -xa 'default lower upper minimal utf-8'
complete -c ntu -s L -d 'List available sequences'
complete -c ntu -l conf -d 'Use specific configuration file' -r -F
complete -c ntu -s q -l quiet -d 'Suppress output (no rename information)'
complete -c ntu -s n -l dry-run -d 'Show what would be renamed without making changes'
complete -c ntu -l no-changes -d 'Alias for --dry-run'
complete -c ntu -s f -l force -d 'Overwrite existing files'
complete -c ntu -s e -l exclude -d 'Exclude files matching pattern' -r
complete -c ntu -s v -l verbose -d 'Show verbose debug information'
complete -c ntu -l modify-root -d 'Allow renaming the root directory'
complete -c ntu -l special -d 'Process symbolic links and special files'
complete -c ntu -l no-color -d 'Disable colored output'
complete -c ntu -s h -l help -d 'Print help information'
complete -c ntu -s V -l version -d 'Print version information'
# File/directory completion for paths
complete -c ntu -a '(__fish_complete_path)' -d 'Path to process'

35
index.md Normal file
View file

@ -0,0 +1,35 @@
# Willkommen bei meinem Projekt
NameToUnix - Rust Command line tool for cleaning up directory & file names according to Linux conventions. Replacing offending characters or spaces (recursive with `-r` flag).
Dieses Projekt bietet unter Linux eine einfache Lösung für das automatische Umbenennen von Verzeichnissen und Dateien nach dem Entpacken von gezippten Windows-Dateien mit Leerzeichen oder Sonderzeichen im Namen.
## Zielsetzung
Das Ziel ist es, diese unkonventionellen Dateinamen automatisch sinnvoll umzubenennen. Ab v1.0.0 ist die rekursive Verarbeitung opt-in (`-r` Flag erforderlich).
## Inhaltsverzeichnis
```text
NameToUnix/
├── .github/ # CI/CD und GitHub-spezifische Dateien
│ └── workflows/
│ └── build.yaml # GitHub Actions Workflow für Build and Release
├── test/ # enthält test-Verzeichnis Skript
│ └── create_test_tree.sh # Bash-Skript erzeugt ein skurriles Testverzeichnis
├── src/ # Quellcode-Verzeichnis
│ ├── main.rs # Haupteinstiegspunkt (bereits vorhanden)
│ ├── cli.rs # CLI-Argumente und Parsing
│ ├── config.rs # Konfigurationsverwaltung
│ └── sanitizer.rs # Kernlogik zur Dateinamenbereinigung
├── .NameToUnix.conf # Konfigurationsdatei (Übersetzungsregeln: 'foo' = 'bar')
├── CONTRIBUTING.md # Contribute-Dokumentation
├── Cargo.lock # Abhängigkeiten
├── Cargo.toml # Projektmetadaten und Abhängigkeiten
├── LICENSE # Lizenzinformationen
├── README.md # Projektdokumentation
├── index.md # diese Datei (german)
└── release.md # Infos über dieses Release (german)
```
## Hinweise zur Nutzung
Um dieses Projekt zu nutzen, folge bitte den Anweisungen in der [README -Datei](README.md).

64
install.sh Executable file
View file

@ -0,0 +1,64 @@
#!/bin/bash
# Installation script for ntu (NameToUnix)
set -e
echo "=== NameToUnix Installation ==="
echo
# Check if binary exists
if [ ! -f "target/release/ntu" ]; then
echo "Error: Binary not found. Please run 'cargo build --release' first."
exit 1
fi
# Install binary
echo "Installing binary..."
sudo cp target/release/ntu /usr/local/bin/
sudo chmod 755 /usr/local/bin/ntu
echo "✓ Binary installed to /usr/local/bin/ntu"
# Install man page
echo "Installing man page..."
sudo cp man/ntu.1 /usr/share/man/man1/
sudo chmod 644 /usr/share/man/man1/ntu.1
sudo mandb -q
echo "✓ Man page installed to /usr/share/man/man1/ntu.1"
# Install config (user-specific)
echo "Installing configuration..."
mkdir -p ~/.config/NameToUnix/
cp .NameToUnix.conf ~/.config/NameToUnix/config.toml
echo "✓ Config installed to ~/.config/NameToUnix/config.toml"
# Install shell completions
echo "Installing shell completions..."
# Bash
if [ -d "/etc/bash_completion.d" ]; then
sudo cp completions/ntu.bash /etc/bash_completion.d/ntu
echo "✓ Bash completion installed"
fi
# Zsh
if [ -d "/usr/share/zsh/site-functions" ]; then
sudo cp completions/_ntu /usr/share/zsh/site-functions/_ntu
echo "✓ Zsh completion installed"
fi
# Fish
if [ -d "/usr/share/fish/vendor_completions.d" ]; then
sudo cp completions/ntu.fish /usr/share/fish/vendor_completions.d/ntu.fish
echo "✓ Fish completion installed"
fi
echo
echo "=== Installation complete ==="
echo
echo "Test the installation:"
echo " ntu --version"
echo " man ntu"
echo
echo "For shell completions to work, restart your shell or run:"
echo " source ~/.bashrc (Bash)"
echo " source ~/.zshrc (Zsh)"

199
man/ntu.1 Normal file
View file

@ -0,0 +1,199 @@
.TH NTU 1 "2025-02-10" "NameToUnix 1.1.0" "User Commands"
.SH NAME
ntu \- sanitize file and directory names to Unix conventions
.SH SYNOPSIS
.B ntu
[\fIOPTIONS\fR] \fIPATH\fR...
.SH DESCRIPTION
.B ntu
(NameToUnix) is a command-line tool that renames files and directories
to make them compatible with Unix/Linux naming conventions. It replaces
spaces with underscores, converts German umlauts to their ASCII equivalents,
and removes or replaces problematic special characters.
.PP
By default, the tool processes only the specified paths and their immediate
children. Use the \fB\-r\fR flag to enable recursive processing of subdirectories.
When recursive, it starts from the deepest level to avoid conflicts with parent
directory renames. It preserves file extensions, including double extensions
like .tar.gz, and handles hidden files correctly.
.SH OPTIONS
.TP
.BR \-r ", " \-\-recursive
Process directories recursively (default: only immediate children)
.TP
.BR \-s ", " \-\-sequence " \fINAME\fR"
Use a specific transformation sequence. Available sequences: default, lower, upper, minimal, utf-8. Use \fB\-L\fR to list all sequences.
.TP
.BR \-L
List all available transformation sequences. Use with \fB\-v\fR for detailed information.
.TP
.BR \-\-conf " \fIFILE\fR"
Use a specific configuration file instead of the default hierarchy
.TP
.BR \-q ", " \-\-quiet
Suppress output (no rename information on stdout)
.TP
.BR \-n ", " \-\-dry\-run
Show what would be renamed without making actual changes (alias: \-\-no\-changes)
.TP
.BR \-f ", " \-\-force
Overwrite existing files if target already exists
.TP
.BR \-e ", " \-\-exclude " \fIPATTERN\fR"
Exclude files matching glob pattern (can be specified multiple times)
.TP
.BR \-v ", " \-\-verbose
Show verbose debug information
.TP
.BR \-\-modify\-root
Allow renaming of the root directory itself (normally skipped)
.TP
.BR \-\-special
Process symbolic links and special files (normally skipped)
.TP
.BR \-\-no\-color
Disable colored output
.TP
.BR \-h ", " \-\-help
Print help information
.TP
.BR \-V ", " \-\-version
Print version information
.SH TRANSFORMATIONS
.B ntu
applies the following transformations to filenames:
.TP
.B Spaces
Replaced with underscores (_)
.TP
.B German Umlauts
ä → ae, ö → oe, ü → ue, Ä → Ae, Ö → Oe, Ü → Ue, ß → ss
.TP
.B Special Characters
Most special characters are replaced with underscores. Some are preserved
in specific contexts (e.g., C++ is kept intact).
.TP
.B Hidden Files
Files starting with a dot (.) are recognized and preserved correctly.
.TP
.B Double Extensions
Extensions like .tar.gz, .tar.bz2, .tar.xz are preserved as a unit.
.TP
.B Parentheses
Replaced with underscores.
.TP
.B Multiple Underscores
Consecutive underscores are collapsed to a single underscore.
.SH SEQUENCES
.B ntu
supports different transformation sequences that can be selected with the \fB\-s\fR option:
.TP
.B default
Standard transformation: spaces become underscores, German umlauts are converted
to ASCII equivalents, special characters are removed or replaced.
.TP
.B lower
Like default, but converts all text to lowercase.
.TP
.B upper
Like default, but converts all text to UPPERCASE.
.TP
.B minimal
Minimal changes: only replaces spaces with underscores, keeps umlauts and
other UTF-8 characters.
.TP
.B utf-8
UTF-8 friendly: keeps umlauts and UTF-8 characters, replaces spaces,
removes special characters.
.SH EXCLUDED PATTERNS
By default, the following directories are automatically excluded:
.PP
.nf
.RS
.git/, .svn/, node_modules/, .cache/, __pycache__/
.RE
.fi
.PP
Additional patterns can be excluded using the \fB\-e\fR option.
.SH PARALLEL PROCESSING
For directories with 100 or more files, \fBntu\fR automatically uses
parallel processing to improve performance. For smaller directories,
it processes files sequentially to avoid overhead.
.SH EXAMPLES
.TP
Rename files in current directory (dry-run):
.B ntu \-n .
.TP
Rename files in specific directory:
.B ntu /path/to/directory
.TP
Exclude specific patterns:
.B ntu \-e "*.tmp" \-e "*.bak" /path/to/directory
.TP
Force overwrite existing files:
.B ntu \-f /path/to/directory
.TP
Process multiple directories:
.B ntu /path/one /path/two /path/three
.TP
Verbose output with no colors:
.B ntu \-v \-\-no\-color /path/to/directory
.TP
Use lowercase sequence:
.B ntu \-r \-s lower /path/to/files
.TP
Minimal mode (only spaces, keep UTF-8):
.B ntu \-s minimal /path/to/files
.TP
UTF-8 friendly mode:
.B ntu \-s utf-8 /path/to/files
.TP
List all available sequences:
.B ntu \-L
.TP
List sequences with details:
.B ntu \-L \-v
.SH CONFIGURATION
.B ntu
looks for configuration files in the following locations (in order):
.PP
.nf
.RS
./.NameToUnix.conf
~/.config/NameToUnix/config.toml
/etc/NameToUnix/config.toml
.RE
.fi
.PP
Configuration files can customize character replacements and other behavior.
See the project documentation for configuration file format.
.SH EXIT STATUS
.TP
.B 0
Success
.TP
.B 1
Error occurred during processing
.SH SAFETY
.B ntu
performs several safety checks:
.IP \(bu 2
Checks if target file already exists (unless \fB\-\-force\fR is used)
.IP \(bu 2
Verifies write permissions before attempting rename
.IP \(bu 2
Processes files from deepest to shallowest to avoid parent conflicts
.IP \(bu 2
Skips special files unless explicitly requested with \fB\-\-special\fR
.SH BUGS
Report bugs at: https://github.com/jamulix/NameToUnix/issues
.SH AUTHOR
Written by Dieter Schlüter <dieter.schlueter@linix.de>
.SH COPYRIGHT
Copyright \(co 2025 Dieter Schlüter. Licensed under MIT License.
.SH SEE ALSO
.BR rename (1),
.BR detox (1),
.BR mv (1)
.PP
Full documentation at: https://github.com/jamulix/NameToUnix

742
release.md Normal file
View file

@ -0,0 +1,742 @@
## Release 0.1.0 2025/03/18
Lass mich die wichtigsten Dateien genauer beschreiben:
## 1. README.md
# NameToUnix
Ein leistungsstarkes Kommandozeilen-Tool zum Bereinigen von Dateinamen gemäß Linux-Konventionen. Es ist sinnvoll,
wenn viele Dateinamen z. B. nach einem Download und Entpacken von Zip-Dateien aus Windows-Dateisystemen
Leerzeichen enthalten. Es erspart enorm viel Zeit durch automatisches Umbenennen, d.h. Ersetzen der störenden Zeichen.
## Funktionen
- Ersetzt Leerzeichen und Sonderzeichen durch Unterstriche
- Konvertiert deutsche Umlaute in ihre ASCII-Pendants (ä → ae, usw.)
- Unterstützt rekursive Verarbeitung von Verzeichnissen
- Bietet Vorschau-Modus ohne tatsächliche Änderungen
- Ermöglicht benutzerdefinierte Ersetzungsregeln über Konfigurationsdatei
- Unterstützt Ausschlussmuster für bestimmte Dateien/Verzeichnisse
## Installation
### Über Cargo
```bash
cargo install NameToUnix
```
### Manueller Build
```bash
git clone https://github.com/username/NameToUnix.git
cd NameToUnix
cargo build --release
```
Die ausführbare Datei wird dann unter `target/release/NameToUnix` erstellt.
## Verwendung
```bash
# Grundlegende Verwendung
NameToUnix /pfad/zu/dateien
# Nur Vorschau der Änderungen ohne tatsächliche Umbenennung
NameToUnix -n /pfad/zu/dateien
# Mehrere Pfade verarbeiten
NameToUnix /pfad1 /pfad2 /pfad3
# Bestimmte Dateien ausschließen
NameToUnix -e "*.tmp" -e "backup_*" /pfad/zu/dateien
# Verbosity erhöhen
NameToUnix -v /pfad/zu/dateien
# Auch das Wurzelverzeichnis umbenennen
NameToUnix --modify-root /pfad/zu/dateien
```
## Konfiguration
Erstelle eine Datei `.NameToUnix.conf` im Arbeitsverzeichnis mit folgendem Inhalt:
```toml
[replacements]
"foo" = "bar"
"alt" = "neu"
".." = "."
"_·_" = "_-_"
"Ä" = "Ae"
"Ö" = "Oe"
"Ü" = "Ue"
"ä" = "ae"
"ö" = "oe"
"ü" = "ue"
"ß" = "ss"
```
## Lizenz
Dieses Projekt steht unter der MIT-Lizenz - siehe die [LICENSE](LICENSE)-Datei für Details.
## Mitwirken
Beiträge sind willkommen! Bitte lies [CONTRIBUTING.md](CONTRIBUTING.md) für Details zum Prozess für Pull Requests.
## 2. .gitignore
```text
/target
**/*.rs.bk
Cargo.lock
.idea/
.vscode/
*.swp
*.swo
.DS_Store
.NameToUnix.conf
/test-files/
```
## 3. LICENSE
```text
MIT License
Copyright (c) 2025 Dieter Schlüter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## 4. src/cli.rs
```rust
use clap::{ArgGroup, Parser};
use std::path::PathBuf;
/// Command-line interface für das Umbenennungsprogramm
#[derive(Parser, Debug, Clone)]
#[clap(about, version, author)]
#[clap(group(
ArgGroup::new("mode")
.args(&["no_changes", "force"])
.multiple(false)
))]
pub struct Cli {
/// Pfade (Dateien und Verzeichnisse) zum rekursiven Anpassen
pub paths: Vec<PathBuf>,
/// Ausgaben unterdrücken (keine Umbenennungsinfos auf stdout)
#[clap(short, long)]
pub quiet: bool,
/// Nur anzeigen, aber keine realen Änderungen vornehmen
#[clap(short, long)]
pub no_changes: bool,
/// Existierende Dateien überschreiben
#[clap(short, long)]
pub force: bool,
/// Zu ignorierende Muster (-e "*.py", mehrere können angegeben werden)
#[clap(short = 'e', long, value_name = "PATTERN")]
pub exclude: Vec<String>,
/// Ausführliche Debug-Informationen
#[clap(short = 'v', long)]
pub verbose: bool,
/// Erlaubt, auch das Wurzelverzeichnis anzupassen
#[clap(long)]
pub modify_root: bool,
}
```
## 5. src/config.rs
```rust
use anyhow::{Context, Result};
use log::{debug, info};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
/// Repräsentiert die Konfiguration, die sich aus einer TOML-Datei laden lässt.
#[derive(Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct Config {
/// Individuelle Ersetzungen, z. B. "foo" => "bar"
pub replacements: HashMap<String, String>,
}
impl Config {
/// Liest eine TOML-Konfigurationsdatei ein, wenn sie existiert.
fn load_internal(path: &Path, verbose: bool) -> Result<Self> {
if path.exists() {
if verbose {
debug!("Lade Konfigurationsdatei: {}", path.display());
}
let content = fs::read_to_string(path).with_context(|| {
format!("Konnte Konfigurationsdatei nicht lesen: {}", path.display())
})?;
let loaded: Self = toml::from_str(&content).with_context(|| {
format!(
"Konnte Konfigurationsdatei nicht parsen: {}",
path.display()
)
})?;
if verbose && !loaded.replacements.is_empty() {
debug!("Eingelesene Replacements:");
for (k, v) in &loaded.replacements {
debug!(" {:?} => {:?}", k, v);
}
}
Ok(loaded)
} else {
Err(anyhow::anyhow!("Datei existiert nicht: {}", path.display()))
}
}
/// Öffentliche Methode zum Laden einer Konfiguration aus einem Pfad
pub fn load(path: &str, verbose: bool) -> Result<Self> {
let cfg_path = Path::new(path);
match Self::load_internal(cfg_path, verbose) {
Ok(config) => Ok(config),
Err(_) => {
if verbose {
info!(
"Keine Konfigurationsdatei '{}' gefunden. Verwende Standardwerte.",
path
);
}
Ok(Self::default())
}
}
}
/// Sucht nach Konfigurationsdateien in verschiedenen Orten und kombiniert sie
pub fn from_default_locations(verbose: bool) -> Result<Self> {
// Prioritätenreihenfolge (später überschreibt früher):
// 1. Standard etc-Verzeichnis (/etc/NameToUnix/config.toml)
// 2. Benutzerverzeichnis (~/.config/NameToUnix/config.toml)
// 3. Arbeitsverzeichnis (./.NameToUnix.conf)
// Die Ladereihenfolge ist so gewählt, dass lokale Einstellungen Vorrang vor
// Benutzereinstellungen haben und diese wiederum Vorrang vor Systemeinstellungen.
// Wir beginnen mit einer leeren Konfiguration
let mut config = Self::default();
let mut config_found = false;
// Standard etc-Verzeichnis
let etc_config = Path::new("/etc/NameToUnix/config.toml");
if etc_config.exists() {
if verbose {
debug!("Lade System-Konfiguration: {}", etc_config.display());
}
if let Ok(etc_conf) = Self::load(etc_config.to_str().unwrap_or_default(), verbose) {
// Füge die Werte zur Konfiguration hinzu
config.replacements.extend(etc_conf.replacements);
config_found = true;
}
} else if verbose {
debug!(
"System-Konfiguration nicht gefunden: {}",
etc_config.display()
);
}
// Benutzerverzeichnis
if let Some(home) = dirs::home_dir() {
let user_config = home.join(".config/NameToUnix/config.toml");
if user_config.exists() {
if verbose {
debug!("Lade Benutzer-Konfiguration: {}", user_config.display());
}
if let Ok(user_conf) = Self::load(user_config.to_str().unwrap_or_default(), verbose)
{
// Überschreibe/ergänze mit Benutzerkonfiguration
config.replacements.extend(user_conf.replacements);
config_found = true;
}
} else if verbose {
debug!(
"Benutzer-Konfiguration nicht gefunden: {}",
user_config.display()
);
}
}
// Arbeitsverzeichnis (höchste Priorität)
let local_config = Path::new(".NameToUnix.conf");
if local_config.exists() {
if verbose {
debug!("Lade lokale Konfiguration: {}", local_config.display());
}
if let Ok(local_conf) = Self::load(local_config.to_str().unwrap_or_default(), verbose) {
// Überschreibe/ergänze mit lokaler Konfiguration
config.replacements.extend(local_conf.replacements);
config_found = true;
}
} else if verbose {
debug!(
"Lokale Konfiguration nicht gefunden: {}",
local_config.display()
);
}
// Gib die kombinierte Konfiguration zurück
if config_found {
if verbose && !config.replacements.is_empty() {
info!(
"Kombinierte Konfiguration enthält {} Ersetzungen",
config.replacements.len()
);
}
} else {
if verbose {
info!("Keine Konfigurationsdateien gefunden. Verwende nur die Standardwerte.");
}
}
Ok(config)
}
}
```
## 6. src/sanitizer.rs
```rust
use crate::config::Config;
use emojis;
use glob::Pattern;
use log::{debug, warn};
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use std::ffi::OsStr;
use std::path::Path;
use unicode_segmentation::UnicodeSegmentation;
use walkdir::DirEntry;
// Regex-Patterns als statische Variablen für bessere Performance
static RE_INVALID: Lazy<Regex> = Lazy::new(|| Regex::new(r"[^\w.\-]").unwrap());
static RE_ADJACENT: Lazy<Regex> = Lazy::new(|| Regex::new(r"_\.|\._").unwrap());
static RE_MULTI: Lazy<Regex> = Lazy::new(|| Regex::new(r"[_\.]{2,}").unwrap());
/// Bereinigt den übergebenen Dateinamen oder Verzeichnisnamen.
pub fn clean_filename(name: &OsStr, config: &Config, verbose: bool) -> Option<String> {
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()),
};
// Platzhalter (C++, c++, C#, c#) anlegen
base = preserve_special_identifiers(&base);
ext = preserve_special_identifiers(&ext);
// 1) Konfig-Replacements zuerst
for (k, v) in &config.replacements {
base = base.replace(k, v);
}
// 2) Dann erst hart-codierte Ersetzungen anwenden
base = apply_hardcoded_replacements(&base);
// 3) Emojis und hochgestellte Zeichen ersetzen
base = replace_emojis_and_superscript(&base);
// 4) Entfernen/Ersetzen aller übrigen ungültigen Zeichen
base = RE_INVALID.replace_all(&base, "_").to_string();
// Ungültige Kombinationen aus Punkt und Unterstrich
base = RE_ADJACENT.replace_all(&base, ".").to_string();
// Mehrfache Punkte/Unterstriche auf einen reduzieren
base = RE_MULTI
.replace_all(
&base,
|caps: &Captures| {
if caps[0].contains('.') {
"."
} else {
"_"
}
},
)
.to_string();
// Führender Punkt soll bleiben, führende Unterstriche sollen verschwinden
base = trim_leading_underscores_preserve_leading_dot(&base);
// Überflüssige Unterstriche und Punkte am Ende beseitigen
base = base.trim_end_matches('_').to_string();
base = base.trim_end_matches('.').to_string();
// Falls komplett geleert, "unnamed"
if base.is_empty() {
base = "unnamed".to_string();
}
// Endgültigen Dateinamen zusammenbauen
let mut result = format!("{}{}", base, ext);
// Platzhalter zurückverwandeln
result = restore_special_identifiers(&result);
// Falls --verbose und sich der Name geändert hat
if verbose && result != original {
debug!("Transformiert: '{}' -> '{}'", original, result);
}
// Keine Änderung -> None zurückgeben
if result == *original {
None
} else {
Some(result)
}
}
/// Schützt spezielle Identifikatoren vor der Umwandlung
fn preserve_special_identifiers(input: &str) -> String {
input
.replace("C++", "CPLUSPLUS")
.replace("c++", "cplusplus")
.replace("C#", "CSHARP")
.replace("c#", "csharp")
}
/// Stellt spezielle Identifikatoren wieder her
fn restore_special_identifiers(input: &str) -> String {
input
.replace("CPLUSPLUS", "C++")
.replace("cplusplus", "c++")
.replace("CSHARP", "C#")
.replace("csharp", "c#")
}
/// Fasst alle fest eingebauten Ersetzungen zusammen.
fn apply_hardcoded_replacements(input: &str) -> String {
input
.replace('\'', "") // Apostroph entfernen
.replace("ˆ", "_")
}
/// Entfernt am Anfang nur Unterstriche, einen führenden Punkt (.) bewahrt es.
fn trim_leading_underscores_preserve_leading_dot(s: &str) -> String {
let mut chars = s.chars().peekable();
let mut result = String::new();
if let Some('.') = chars.peek() {
// Nimm den Punkt
result.push('.');
chars.next();
// Entferne anschließend führende Unterstriche hinter dem Punkt
while let Some('_') = chars.peek() {
chars.next();
}
} else {
// Entferne führende Unterstriche
while let Some('_') = chars.peek() {
chars.next();
}
}
// Restliche Zeichen anfügen
result.extend(chars);
result
}
/// Ersetzt Emojis und hochgestellte Zeichen (z. B. ²³⁴) durch '_'.
fn replace_emojis_and_superscript(input: &str) -> String {
input
.graphemes(true)
.map(|g| {
if emojis::get(g).is_some() {
"_".to_string()
} else if is_superscript(g) {
"_".to_string()
} else {
g.to_string()
}
})
.collect()
}
/// Prüft, ob alle Zeichen ein Superscript sind (z. B. ²³⁴).
fn is_superscript(g: &str) -> bool {
g.chars().all(|c| {
c == '\u{00AA}'
|| c == '\u{00BA}'
|| (c >= '\u{00B2}' && c <= '\u{00B3}')
|| c == '\u{00B9}'
|| (c >= '\u{2070}' && c <= '\u{209F}')
})
}
/// Prüft, ob der Pfad aufgrund der Ausschlussmuster ignoriert werden soll.
pub fn is_excluded(entry: &DirEntry, patterns: &[Pattern]) -> bool {
let path = entry.path();
patterns.iter().any(|p| p.matches_path(path))
}
/// Überprüft, ob eine Umbenennungsoperation sicher ist
pub fn is_safe_rename(src: &Path, dst: &Path, force: bool) -> bool {
if src.exists() && dst.exists() && !force {
warn!(
"Ziel existiert bereits und --force nicht gesetzt: {}",
dst.display()
);
return false;
}
// Prüfen auf zusätzliche Sicherheitsrisiken
// z.B. Systemdateien, schreibgeschützte Verzeichnisse, etc.
true
}
```
## 7. src/main.rs
```rust
// Verwende nun die Module
mod cli;
mod config;
mod sanitizer;
use anyhow::{Context, Result};
use clap::Parser;
use cli::Cli;
use config::Config;
use glob::Pattern;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, error, info};
use sanitizer::{clean_filename, is_excluded, is_safe_rename};
use std::fs;
use walkdir::WalkDir;
/// Startpunkt des Programms
fn main() -> Result<()> {
// Initialisiere Logger
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// Argumente parsen
let args = Cli::parse();
// Optional Konfigurationsdatei laden
let config = Config::from_default_locations(args.verbose)?;
// let config = Config::load(".NameToUnix.conf", args.verbose)?;
// Ausschlussmuster (Glob-Patterns) vorbereiten
let exclude_patterns = args
.exclude
.iter()
.map(|pattern| {
Pattern::new(pattern)
.with_context(|| format!("Ungültiges Ausschlussmuster: {}", pattern))
})
.collect::<Result<Vec<_>>>()?;
if args.verbose && !exclude_patterns.is_empty() {
debug!("Folgende Exclude-Pattern werden genutzt:");
for p in &exclude_patterns {
debug!(" {}", p.as_str());
}
}
// Für alle angegebenen Pfade
for path in &args.paths {
// Alle Einträge sammeln, damit zuerst die tiefsten umbenannt werden
let mut entries = Vec::new();
for entry_result in WalkDir::new(path)
.into_iter()
.filter_entry(|e| !is_excluded(e, &exclude_patterns))
{
if let Ok(entry) = entry_result {
entries.push(entry);
} else if let Err(e) = entry_result {
error!("Fehler beim Durchlaufen von {}: {}", path.display(), e);
}
}
// Aufsteigend nach Tiefe sortieren, dann umkehren => tiefste Einträge zuerst
entries.sort_by_key(|e| e.depth());
entries.reverse();
// Fortschrittsbalken bei größeren Dateimengen
let progress_bar = if !args.quiet && entries.len() > 50 {
let bar = ProgressBar::new(entries.len() as u64);
bar.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")?
.progress_chars("#>-"));
Some(bar)
} else {
None
};
// Umbenennen in absteigender Tiefe
for entry in entries {
if let Some(bar) = &progress_bar {
bar.inc(1);
}
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;
}
}
// Dateiname (oder Verzeichnisname) ermitteln
let filename = old_path.file_name().ok_or_else(|| {
anyhow::anyhow!(
"Konnte Dateinamen nicht ermitteln für: {}",
old_path.display()
)
})?;
// Verarbeiten und ggf. umbenennen
if let Some(new_name) = clean_filename(filename, &config, args.verbose) {
let new_path = old_path.with_file_name(&new_name);
if !args.quiet {
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 let Some(bar) = &progress_bar {
bar.finish_with_message("Umbenennung abgeschlossen");
}
}
Ok(())
}
```
## 8. ./github/workflows/build.yaml -- GitHub Actions Workflow für CI/CD
```yaml
name: Build and Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
rust: [stable]
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
components: rustfmt, clippy
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- name: Format check
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- name: Clippy check
uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: --verbose
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --verbose
```
## Zusammenfassung
Diese Dateien bilden zusammen ein vollständiges, gut strukturiertes Rust-Projekt für den `NameToUnix`.
Die wichtigsten Aspekte sind:
1. **Modularisierung**: Der Code ist in logische Module aufgeteilt (cli.rs, config.rs, sanitizer.rs)
2. **Verbesserte Fehlerbehandlung**: Verwendung von `anyhow` für bessere Fehlermeldungen
3. **Dokumentation**: Ausführliche README.md mit Anwendungsbeispielen
4. **Tests**: Test-Skript zur Überprüfung der Funktionalität
5. **CI/CD**: Aktionen für automatisierte Tests und Builds
6. **Konfiguration**: Erweiterte Konfigurationsmöglichkeiten
7. **Benutzerfreundlichkeit**: Fortschrittsbalken für große Dateimengen
Diese Struktur folgt den Rust-Best-Practices und macht das Projekt wartbar, erweiterbar und benutzerfreundlich.

63
src/cli.rs Normal file
View file

@ -0,0 +1,63 @@
use clap::{ArgGroup, Parser};
use std::path::PathBuf;
/// Command-line interface für das Umbenennungsprogramm
#[derive(Parser, Debug, Clone)]
#[clap(about, version, author)]
#[clap(group(
ArgGroup::new("mode")
.args(&["dry_run", "force"])
.multiple(false)
))]
pub struct Cli {
/// Pfade (Dateien und Verzeichnisse) zum rekursiven Anpassen
pub paths: Vec<PathBuf>,
/// Explizite Konfigurationsdatei (bypassed Standard-Hierarchie)
#[clap(long = "conf", value_name = "FILE")]
pub config_file: Option<PathBuf>,
/// Rekursive Verarbeitung von Unterverzeichnissen aktivieren
#[clap(short = 'r', long)]
pub recursive: bool,
/// Wählt eine Transformations-Sequenz aus (default, lower, upper, minimal, utf-8)
#[clap(short = 's', long, value_name = "NAME")]
pub sequence: Option<String>,
/// Listet alle verfügbaren Sequences auf
#[clap(short = 'L')]
pub list_sequences: bool,
/// Ausgaben unterdrücken (keine Umbenennungsinfos auf stdout)
#[clap(short, long)]
pub quiet: bool,
/// Nur anzeigen, aber keine realen Änderungen vornehmen (dry-run)
#[clap(short = 'n', long = "dry-run", alias = "no-changes")]
pub dry_run: bool,
/// Existierende Dateien überschreiben
#[clap(short, long)]
pub force: bool,
/// Zu ignorierende Muster (-e "*.py", mehrere können angegeben werden)
#[clap(short = 'e', long, value_name = "PATTERN")]
pub exclude: Vec<String>,
/// Ausführliche Debug-Informationen
#[clap(short = 'v', long)]
pub verbose: bool,
/// Erlaubt, auch das Wurzelverzeichnis anzupassen
#[clap(long)]
pub modify_root: bool,
/// Auch symbolische Links und Special Files verarbeiten
#[clap(long)]
pub special: bool,
/// Deaktiviert farbige Ausgabe
#[clap(long)]
pub no_color: bool,
}

157
src/config.rs Normal file
View file

@ -0,0 +1,157 @@
use anyhow::{Context, Result};
use log::{debug, info};
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
/// Repräsentiert die Konfiguration, die sich aus einer TOML-Datei laden lässt.
#[derive(Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct Config {
/// Individuelle Ersetzungen, z. B. "foo" => "bar"
pub replacements: HashMap<String, String>,
}
impl Config {
/// Liest eine TOML-Konfigurationsdatei ein, wenn sie existiert.
fn load_internal(path: &Path, verbose: bool) -> Result<Self> {
if path.exists() {
if verbose {
debug!("Lade Konfigurationsdatei: {}", path.display());
}
let content = fs::read_to_string(path).with_context(|| {
format!("Konnte Konfigurationsdatei nicht lesen: {}", path.display())
})?;
let loaded: Self = toml::from_str(&content).with_context(|| {
format!(
"Konnte Konfigurationsdatei nicht parsen: {}",
path.display()
)
})?;
if verbose && !loaded.replacements.is_empty() {
debug!("Eingelesene Replacements:");
for (k, v) in &loaded.replacements {
debug!(" {:?} => {:?}", k, v);
}
}
Ok(loaded)
} else {
Err(anyhow::anyhow!("Datei existiert nicht: {}", path.display()))
}
}
/// Öffentliche Methode zum Laden einer Konfiguration aus einem Pfad
pub fn load(path: &str, verbose: bool) -> Result<Self> {
let cfg_path = Path::new(path);
match Self::load_internal(cfg_path, verbose) {
Ok(config) => Ok(config),
Err(_) => {
if verbose {
info!(
"Keine Konfigurationsdatei '{}' gefunden. Verwende Standardwerte.",
path
);
}
Ok(Self::default())
}
}
}
/// Lädt Konfiguration aus spezifischer Datei (fehlschlägt bei nicht-existierender Datei)
pub fn from_file(path: &Path, verbose: bool) -> Result<Self> {
if !path.exists() {
return Err(anyhow::anyhow!(
"Konfigurationsdatei nicht gefunden: {}",
path.display()
));
}
Self::load_internal(path, verbose)
}
/// Sucht nach Konfigurationsdateien in verschiedenen Orten und kombiniert sie
pub fn from_default_locations(verbose: bool) -> Result<Self> {
// Prioritätenreihenfolge (später überschreibt früher):
// 1. Standard etc-Verzeichnis (/etc/NameToUnix/config.toml)
// 2. Benutzerverzeichnis (~/.config/NameToUnix/config.toml)
// 3. Arbeitsverzeichnis (./.NameToUnix.conf)
// Die Ladereihenfolge ist so gewählt, dass lokale Einstellungen Vorrang vor
// Benutzereinstellungen haben und diese wiederum Vorrang vor Systemeinstellungen.
// Wir beginnen mit einer leeren Konfiguration
let mut config = Self::default();
let mut config_found = false;
// Standard etc-Verzeichnis
let etc_config = Path::new("/etc/NameToUnix/config.toml");
if etc_config.exists() {
if verbose {
debug!("Lade System-Konfiguration: {}", etc_config.display());
}
if let Ok(etc_conf) = Self::load(etc_config.to_str().unwrap_or_default(), verbose) {
// Füge die Werte zur Konfiguration hinzu
config.replacements.extend(etc_conf.replacements);
config_found = true;
}
} else if verbose {
debug!(
"System-Konfiguration nicht gefunden: {}",
etc_config.display()
);
}
// Benutzerverzeichnis
if let Some(home) = dirs::home_dir() {
let user_config = home.join(".config/NameToUnix/config.toml");
if user_config.exists() {
if verbose {
debug!("Lade Benutzer-Konfiguration: {}", user_config.display());
}
if let Ok(user_conf) = Self::load(user_config.to_str().unwrap_or_default(), verbose)
{
// Überschreibe/ergänze mit Benutzerkonfiguration
config.replacements.extend(user_conf.replacements);
config_found = true;
}
} else if verbose {
debug!(
"Benutzer-Konfiguration nicht gefunden: {}",
user_config.display()
);
}
}
// Arbeitsverzeichnis (höchste Priorität)
let local_config = Path::new(".NameToUnix.conf");
if local_config.exists() {
if verbose {
debug!("Lade lokale Konfiguration: {}", local_config.display());
}
if let Ok(local_conf) = Self::load(local_config.to_str().unwrap_or_default(), verbose) {
// Überschreibe/ergänze mit lokaler Konfiguration
config.replacements.extend(local_conf.replacements);
config_found = true;
}
} else if verbose {
debug!(
"Lokale Konfiguration nicht gefunden: {}",
local_config.display()
);
}
// Gib die kombinierte Konfiguration zurück
if config_found {
if verbose && !config.replacements.is_empty() {
info!(
"Kombinierte Konfiguration enthält {} Ersetzungen",
config.replacements.len()
);
}
} else if verbose {
info!("Keine Konfigurationsdateien gefunden. Verwende nur die Standardwerte.");
}
Ok(config)
}
}

326
src/main.rs Normal file
View file

@ -0,0 +1,326 @@
// Verwende nun die Module
mod cli;
mod config;
mod sanitizer;
use anyhow::{Context, Result};
use clap::Parser;
use cli::Cli;
use colored::*;
use config::Config;
use glob::Pattern;
use indicatif::{ProgressBar, ProgressStyle};
use log::{debug, error, info};
use rayon::prelude::*;
use sanitizer::{clean_filename, is_excluded, is_safe_rename, Sequence};
use std::fs;
use std::io::IsTerminal;
use std::path::PathBuf;
use walkdir::WalkDir;
// Standard-Ausschlussmuster (ähnlich wie detox)
const DEFAULT_EXCLUDES: &[&str] = &[
".git",
".git/**",
".svn",
".svn/**",
"node_modules",
"node_modules/**",
".cache",
".cache/**",
"__pycache__",
"__pycache__/**",
];
// Schwellwert für parallele Verarbeitung (bei weniger Dateien lohnt sich Overhead nicht)
const PARALLEL_THRESHOLD: usize = 100;
/// Repräsentiert eine geplante Umbenennungsoperation
#[derive(Debug)]
struct RenameOperation {
old_path: PathBuf,
new_path: PathBuf,
}
/// Prüft ob farbige Ausgabe aktiviert sein soll
fn should_use_color(no_color_flag: bool) -> bool {
!no_color_flag && std::io::stdout().is_terminal()
}
/// Startpunkt des Programms
fn main() -> Result<()> {
// Initialisiere Logger
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// Argumente parsen
let args = Cli::parse();
// Farben konfigurieren
if !should_use_color(args.no_color) {
colored::control::set_override(false);
}
// -L Option: Liste Sequences und beende
if args.list_sequences {
list_sequences(&args);
return Ok(());
}
// Sequence auswählen
let sequence = if let Some(seq_name) = &args.sequence {
Sequence::find(seq_name).ok_or_else(|| {
anyhow::anyhow!(
"Unbekannte Sequence: '{}'. Nutze -L um verfügbare Sequences anzuzeigen.",
seq_name
)
})?
} else {
Sequence::default()
};
if args.verbose {
info!("Verwende Sequence: {}", sequence.name);
}
// Config-Datei laden: entweder --conf oder Standard-Hierarchie
let config = if let Some(config_path) = &args.config_file {
Config::from_file(config_path, args.verbose)
.with_context(|| format!("Fehler beim Laden der Konfiguration: {}", config_path.display()))?
} else {
Config::from_default_locations(args.verbose)?
};
// Ausschlussmuster (Glob-Patterns) vorbereiten - Default-Excludes + User-Excludes
let mut all_excludes = DEFAULT_EXCLUDES
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>();
all_excludes.extend(args.exclude.clone());
let exclude_patterns = all_excludes
.iter()
.map(|pattern| {
Pattern::new(pattern)
.with_context(|| format!("Ungültiges Ausschlussmuster: {}", pattern))
})
.collect::<Result<Vec<_>>>()?;
if args.verbose && !exclude_patterns.is_empty() {
debug!("Folgende Exclude-Pattern werden genutzt:");
for p in &exclude_patterns {
debug!(" {}", p.as_str());
}
}
// Für alle angegebenen Pfade
for path in &args.paths {
// Alle Einträge sammeln, damit zuerst die tiefsten umbenannt werden
let mut entries = Vec::new();
let walker = if args.recursive {
// Recursive: unbegrenzte Tiefe
WalkDir::new(path)
} else {
// Non-recursive: max_depth(1) verarbeitet nur direkte Kinder
WalkDir::new(path).max_depth(1)
};
for entry_result in walker
.into_iter()
.filter_entry(|e| !is_excluded(e, &exclude_patterns))
{
if let Ok(entry) = entry_result {
entries.push(entry);
} else if let Err(e) = entry_result {
error!("{}", format!("Fehler beim Durchlaufen von {}: {}", path.display(), e).red());
}
}
// Aufsteigend nach Tiefe sortieren, dann umkehren => tiefste Einträge zuerst
entries.sort_by_key(|e| e.depth());
entries.reverse();
// Fortschrittsbalken bei größeren Dateimengen
let progress_bar = if !args.quiet && entries.len() > 50 {
let bar = ProgressBar::new(entries.len() as u64);
bar.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta}) {msg}")?
.progress_chars("#>-"));
Some(bar)
} else {
None
};
// Berechne Umbenennungen (parallel bei vielen Dateien)
let rename_ops: Vec<RenameOperation> = if entries.len() >= PARALLEL_THRESHOLD {
// Parallel mit rayon
entries
.par_iter()
.filter_map(|entry| {
let old_path = entry.path();
// Ebenentiefe 0 -> überspringen
if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root {
return None;
}
// Special Files nur mit --special
let file_type = entry.file_type();
if !args.special && (!file_type.is_file() && !file_type.is_dir()) {
return None;
}
// Dateiname ermitteln und bereinigen
let filename = old_path.file_name()?;
let new_name = clean_filename(filename, &config, &sequence, false)?;
let new_path = old_path.with_file_name(&new_name);
Some(RenameOperation {
old_path: old_path.to_path_buf(),
new_path,
})
})
.collect()
} else {
// Sequenziell bei wenigen Dateien
entries
.iter()
.filter_map(|entry| {
let old_path = entry.path();
if entry.depth() == 0 && entry.file_type().is_dir() && !args.modify_root {
return None;
}
let file_type = entry.file_type();
if !args.special && (!file_type.is_file() && !file_type.is_dir()) {
return None;
}
let filename = old_path.file_name()?;
let new_name = clean_filename(filename, &config, &sequence, false)?;
let new_path = old_path.with_file_name(&new_name);
Some(RenameOperation {
old_path: old_path.to_path_buf(),
new_path,
})
})
.collect()
};
// Statistiken
let total_processed = entries.len();
let total_planned = rename_ops.len();
let mut renamed_count = 0;
let mut skipped_count = 0;
// Umbenennungen sequenziell ausführen
for op in rename_ops {
if let Some(bar) = &progress_bar {
bar.inc(1);
}
if !args.quiet {
if args.dry_run {
info!("{} {} {}",
op.old_path.display().to_string().dimmed(),
"->".yellow(),
op.new_path.display().to_string().yellow()
);
} else {
info!("{} {} {}",
op.old_path.display().to_string().dimmed(),
"->".green(),
op.new_path.display().to_string().green()
);
}
}
if args.verbose {
debug!("Rename: {:?} -> {:?}", op.old_path, op.new_path);
}
if !args.dry_run {
if is_safe_rename(&op.old_path, &op.new_path, args.force) {
match fs::rename(&op.old_path, &op.new_path) {
Ok(_) => renamed_count += 1,
Err(e) => {
error!("{}",
format!(
"Fehler beim Umbenennen: {} -> {}: {}",
op.old_path.display(),
op.new_path.display(),
e
).red()
);
skipped_count += 1;
}
}
} else {
skipped_count += 1;
}
}
}
if let Some(bar) = &progress_bar {
bar.finish_with_message("Verarbeitung abgeschlossen");
}
// Zusammenfassung ausgeben (außer im quiet mode)
if !args.quiet {
info!("");
info!("{}", format!("=== Zusammenfassung für {} ===", path.display()).cyan().bold());
info!("Verarbeitete Dateien/Verzeichnisse: {}", total_processed.to_string().cyan());
info!("Umbenennungen geplant: {}", total_planned.to_string().cyan());
if args.dry_run {
info!("Modus: {}", "Dry-run (keine Änderungen)".yellow());
} else {
info!("Erfolgreich umbenannt: {}", renamed_count.to_string().green().bold());
if skipped_count > 0 {
info!("Übersprungen/Fehler: {}", skipped_count.to_string().red());
}
}
}
}
Ok(())
}
/// Listet alle verfügbaren Sequences auf
fn list_sequences(args: &Cli) {
println!("Verfügbare Sequences:");
println!();
for seq in Sequence::all() {
println!(" {}", seq.name.bold());
if args.verbose {
println!(" Description: {}", seq.description);
println!(
" Umlauts → ASCII: {}",
if seq.apply_umlauts { "yes" } else { "no" }
);
println!(" Case transform: {:?}", seq.apply_case);
println!(
" Emoji handling: {}",
if seq.apply_emojis {
"replace"
} else {
"keep"
}
);
println!(
" Mode: {}",
if seq.minimal_mode { "minimal" } else { "full" }
);
} else {
println!(" {}", seq.description);
}
println!();
}
if !args.verbose {
println!("Nutze -L -v für detaillierte Informationen über jede Sequence.");
}
}

613
src/sanitizer.rs Normal file
View file

@ -0,0 +1,613 @@
use crate::config::Config;
use glob::Pattern;
use log::{debug, warn};
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use std::ffi::OsStr;
use std::path::Path;
use unicode_segmentation::UnicodeSegmentation;
use walkdir::DirEntry;
// Regex-Patterns als statische Variablen für bessere Performance
static RE_INVALID: Lazy<Regex> = Lazy::new(|| Regex::new(r"[^\w.\-]").unwrap());
static RE_ADJACENT: Lazy<Regex> = Lazy::new(|| Regex::new(r"_\.|\._").unwrap());
static RE_MULTI: Lazy<Regex> = Lazy::new(|| Regex::new(r"[_\.]{2,}").unwrap());
/// Repräsentiert eine Transformations-Sequenz
#[derive(Debug, Clone)]
pub struct Sequence {
pub name: &'static str,
pub description: &'static str,
pub apply_umlauts: bool,
pub apply_case: CaseTransform,
pub apply_emojis: bool,
pub minimal_mode: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CaseTransform {
None,
Lower,
Upper,
}
impl Sequence {
/// Gibt alle verfügbaren Sequences zurück
pub fn all() -> Vec<Sequence> {
vec![
Sequence {
name: "default",
description: "Standard transformation: spaces→underscores, umlauts→ASCII, remove special chars",
apply_umlauts: true,
apply_case: CaseTransform::None,
apply_emojis: true,
minimal_mode: false,
},
Sequence {
name: "lower",
description: "Like default, but convert everything to lowercase",
apply_umlauts: true,
apply_case: CaseTransform::Lower,
apply_emojis: true,
minimal_mode: false,
},
Sequence {
name: "upper",
description: "Like default, but convert everything to UPPERCASE",
apply_umlauts: true,
apply_case: CaseTransform::Upper,
apply_emojis: true,
minimal_mode: false,
},
Sequence {
name: "minimal",
description: "Minimal changes: only replace spaces, keep umlauts and UTF-8",
apply_umlauts: false,
apply_case: CaseTransform::None,
apply_emojis: false,
minimal_mode: true,
},
Sequence {
name: "utf-8",
description: "UTF-8 friendly: spaces→underscores, keep umlauts, remove special chars",
apply_umlauts: false,
apply_case: CaseTransform::None,
apply_emojis: true,
minimal_mode: false,
},
]
}
/// Findet eine Sequence nach Namen
pub fn find(name: &str) -> Option<Sequence> {
Self::all().into_iter().find(|s| s.name == name)
}
/// Gibt die Default-Sequence zurück
pub fn default() -> Sequence {
Self::find("default").unwrap()
}
}
// Bekannte Doppel-Extensions (z.B. .tar.gz)
const DOUBLE_EXTENSIONS: &[&str] = &[
".tar.gz",
".tar.bz2",
".tar.xz",
".tar.zst",
".tar.lz",
".tar.Z",
];
/// Trennt Dateiname in Basis und Extension, berücksichtigt Doppel-Extensions
fn split_filename(filename: &str) -> (String, String) {
// Prüfe auf bekannte Doppel-Extensions
for double_ext in DOUBLE_EXTENSIONS {
if filename.ends_with(double_ext) {
let base_len = filename.len() - double_ext.len();
if base_len > 0 {
return (
filename[..base_len].to_string(),
double_ext.to_string(),
);
}
}
}
// Standard-Fall: nur letzte Extension
match filename.rsplit_once('.') {
Some((b, e)) if !b.is_empty() => (b.to_string(), format!(".{e}")),
_ => (filename.to_string(), String::new()),
}
}
/// Bereinigt den übergebenen Dateinamen mit gegebener Sequence.
pub fn clean_filename(
name: &OsStr,
config: &Config,
sequence: &Sequence,
verbose: bool,
) -> Option<String> {
let original = name.to_string_lossy();
// 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) = split_filename(rest);
// Platzhalter (C++, c++, C#, c#) anlegen
base = preserve_special_identifiers(&base);
ext = preserve_special_identifiers(&ext);
// 1) Config-Replacements anwenden (immer zuerst)
for (k, v) in &config.replacements {
base = base.replace(k, v);
}
// 2) Sequence-basierte Umlaut-Ersetzung
if sequence.apply_umlauts {
base = apply_umlaut_replacements(&base);
}
// 3) Hardcoded replacements (Apostroph etc.)
base = apply_hardcoded_replacements(&base);
// 4) Case-Transformation (auf base UND extension anwenden)
match sequence.apply_case {
CaseTransform::Lower => {
base = base.to_lowercase();
ext = ext.to_lowercase();
}
CaseTransform::Upper => {
base = base.to_uppercase();
ext = ext.to_uppercase();
}
CaseTransform::None => {}
}
// 5) Emojis ersetzen (wenn aktiviert)
if sequence.apply_emojis {
base = replace_emojis_and_superscript(&base);
}
// 6) Ungültige Zeichen behandeln
if sequence.minimal_mode {
// Minimal: Nur Leerzeichen und gefährliche Zeichen
base = base.replace(' ', "_");
// Entferne nur absolut gefährliche Zeichen
base = base
.replace('/', "_")
.replace('\\', "_")
.replace('\0', "_")
.replace('\n', "_");
} else {
// Standard: Alle ungültigen Zeichen → Unterstrich
base = RE_INVALID.replace_all(&base, "_").to_string();
}
// Ungültige Kombinationen aus Punkt und Unterstrich
base = RE_ADJACENT.replace_all(&base, ".").to_string();
// Mehrfache Punkte/Unterstriche auf einen reduzieren
base = RE_MULTI
.replace_all(&base, |caps: &Captures| {
if caps[0].contains('.') {
"."
} else {
"_"
}
})
.to_string();
// Führender Punkt soll bleiben, führende Unterstriche sollen verschwinden
base = trim_leading_underscores_preserve_leading_dot(&base);
// Überflüssige Unterstriche und Punkte am Ende beseitigen
base = base.trim_end_matches('_').to_string();
base = base.trim_end_matches('.').to_string();
// Falls komplett geleert, "unnamed"
if base.is_empty() {
base = "unnamed".to_string();
}
// Endgültigen Dateinamen zusammenbauen (hidden_prefix wieder hinzufügen)
let mut result = format!("{}{}{}", hidden_prefix, base, ext);
// Platzhalter zurückverwandeln
result = restore_special_identifiers(&result);
// Falls --verbose und sich der Name geändert hat
if verbose && result != original {
debug!("Transformiert: '{}' -> '{}'", original, result);
}
// Keine Änderung -> None zurückgeben
if result == *original {
None
} else {
Some(result)
}
}
/// Schützt spezielle Identifikatoren vor der Umwandlung
fn preserve_special_identifiers(input: &str) -> String {
input
.replace("C++", "CPLUSPLUS")
.replace("c++", "cplusplus")
.replace("C#", "CSHARP")
.replace("c#", "csharp")
}
/// Stellt spezielle Identifikatoren wieder her
fn restore_special_identifiers(input: &str) -> String {
input
.replace("CPLUSPLUS", "C++")
.replace("cplusplus", "c++")
.replace("CSHARP", "C#")
.replace("csharp", "c#")
}
/// Fasst alle fest eingebauten Ersetzungen zusammen.
fn apply_hardcoded_replacements(input: &str) -> String {
input
.replace('\'', "") // Apostroph entfernen
.replace("ˆ", "_")
}
/// Ersetzt deutsche Umlaute durch ASCII-Äquivalente
fn apply_umlaut_replacements(input: &str) -> String {
input
.replace("ä", "ae")
.replace("ö", "oe")
.replace("ü", "ue")
.replace("Ä", "Ae")
.replace("Ö", "Oe")
.replace("Ü", "Ue")
.replace("ß", "ss")
}
/// Entfernt am Anfang nur Unterstriche, einen führenden Punkt (.) bewahrt es.
fn trim_leading_underscores_preserve_leading_dot(s: &str) -> String {
let mut chars = s.chars().peekable();
let mut result = String::new();
if let Some('.') = chars.peek() {
// Nimm den Punkt
result.push('.');
chars.next();
// Entferne anschließend führende Unterstriche hinter dem Punkt
while let Some('_') = chars.peek() {
chars.next();
}
} else {
// Entferne führende Unterstriche
while let Some('_') = chars.peek() {
chars.next();
}
}
// Restliche Zeichen anfügen
result.extend(chars);
result
}
/// Ersetzt Emojis und hochgestellte Zeichen (z. B. ²³⁴) durch '_'.
fn replace_emojis_and_superscript(input: &str) -> String {
input
.graphemes(true)
.map(|g| {
if emojis::get(g).is_some() || is_superscript(g) {
"_".to_string()
} else {
g.to_string()
}
})
.collect()
}
/// Prüft, ob alle Zeichen ein Superscript sind (z. B. ²³⁴).
fn is_superscript(g: &str) -> bool {
g.chars().all(|c| {
c == '\u{00AA}'
|| c == '\u{00BA}'
|| ('\u{00B2}'..='\u{00B3}').contains(&c)
|| c == '\u{00B9}'
|| ('\u{2070}'..='\u{209F}').contains(&c)
})
}
/// Prüft, ob der Pfad aufgrund der Ausschlussmuster ignoriert werden soll.
pub fn is_excluded(entry: &DirEntry, patterns: &[Pattern]) -> bool {
let path = entry.path();
patterns.iter().any(|p| p.matches_path(path))
}
/// Überprüft, ob eine Umbenennungsoperation sicher ist
pub fn is_safe_rename(src: &Path, dst: &Path, force: bool) -> bool {
// Prüfen ob Ziel bereits existiert
if src.exists() && dst.exists() && !force {
warn!(
"Ziel existiert bereits und --force nicht gesetzt: {}",
dst.display()
);
return false;
}
// Prüfen ob Quell-Datei überhaupt existiert
if !src.exists() {
warn!("Quell-Datei existiert nicht: {}", src.display());
return false;
}
// Prüfen ob Parent-Verzeichnis der Quelle schreibbar ist
if let Some(parent) = src.parent() {
match std::fs::metadata(parent) {
Ok(metadata) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = metadata.permissions();
if permissions.mode() & 0o200 == 0 {
warn!(
"Keine Schreibrechte im Verzeichnis: {}",
parent.display()
);
return false;
}
}
}
Err(e) => {
warn!(
"Kann Metadaten des Verzeichnisses nicht lesen {}: {}",
parent.display(),
e
);
return false;
}
}
}
// Prüfen ob Parent-Verzeichnis des Ziels schreibbar ist (falls unterschiedlich)
if let Some(dst_parent) = dst.parent() {
if src.parent() != Some(dst_parent) {
match std::fs::metadata(dst_parent) {
Ok(metadata) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = metadata.permissions();
if permissions.mode() & 0o200 == 0 {
warn!(
"Keine Schreibrechte im Ziel-Verzeichnis: {}",
dst_parent.display()
);
return false;
}
}
}
Err(e) => {
warn!(
"Kann Metadaten des Ziel-Verzeichnisses nicht lesen {}: {}",
dst_parent.display(),
e
);
return false;
}
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsStr;
fn make_test_config() -> Config {
Config::default()
}
#[test]
fn test_clean_filename_basic() {
let config = Config::default();
let sequence = Sequence::default();
// Spaces should become underscores
assert_eq!(
clean_filename(OsStr::new("test file.txt"), &config, &sequence, false),
Some("test_file.txt".to_string())
);
// Parentheses should become underscores
assert_eq!(
clean_filename(OsStr::new("file (1).txt"), &config, &sequence, false),
Some("file_1.txt".to_string())
);
// Multiple underscores should be collapsed
assert_eq!(
clean_filename(OsStr::new("test__file.txt"), &config, &sequence, false),
Some("test_file.txt".to_string())
);
}
#[test]
fn test_clean_filename_hidden_files() {
let config = Config::default();
let sequence = Sequence::default();
// Hidden files should keep their leading dot
assert_eq!(
clean_filename(OsStr::new(".gitignore"), &config, &sequence, false),
None // No change needed
);
// Hidden files with spaces
assert_eq!(
clean_filename(OsStr::new(".my config"), &config, &sequence, false),
Some(".my_config".to_string())
);
// Hidden files with extension
assert_eq!(
clean_filename(OsStr::new(".test file.txt"), &config, &sequence, false),
Some(".test_file.txt".to_string())
);
// Multiple leading dots
assert_eq!(
clean_filename(OsStr::new("...strange"), &config, &sequence, false),
Some(".unnamed.strange".to_string())
);
}
#[test]
fn test_clean_filename_umlauts() {
let config = make_test_config();
let sequence = Sequence::default();
// German umlauts
assert_eq!(
clean_filename(OsStr::new("Müller.pdf"), &config, &sequence, false),
Some("Mueller.pdf".to_string())
);
assert_eq!(
clean_filename(OsStr::new("schön.txt"), &config, &sequence, false),
Some("schoen.txt".to_string())
);
assert_eq!(
clean_filename(OsStr::new("Größe.doc"), &config, &sequence, false),
Some("Groesse.doc".to_string())
);
}
#[test]
fn test_clean_filename_extensions() {
let config = Config::default();
let sequence = Sequence::default();
// Single extension
assert_eq!(
clean_filename(OsStr::new("test file.txt"), &config, &sequence, false),
Some("test_file.txt".to_string())
);
// Double extension with spaces in base name
assert_eq!(
clean_filename(OsStr::new("my archive.tar.gz"), &config, &sequence, false),
Some("my_archive.tar.gz".to_string())
);
// Other double extensions
assert_eq!(
clean_filename(OsStr::new("backup file.tar.bz2"), &config, &sequence, false),
Some("backup_file.tar.bz2".to_string())
);
assert_eq!(
clean_filename(OsStr::new("data set.tar.xz"), &config, &sequence, false),
Some("data_set.tar.xz".to_string())
);
// Multiple dots (not a double extension)
assert_eq!(
clean_filename(OsStr::new("foo..bar.txt"), &config, &sequence, false),
Some("foo.bar.txt".to_string())
);
}
#[test]
fn test_split_filename() {
// Double extensions
assert_eq!(
split_filename("archive.tar.gz"),
("archive".to_string(), ".tar.gz".to_string())
);
assert_eq!(
split_filename("backup.tar.bz2"),
("backup".to_string(), ".tar.bz2".to_string())
);
// Single extension
assert_eq!(
split_filename("file.txt"),
("file".to_string(), ".txt".to_string())
);
// No extension
assert_eq!(
split_filename("README"),
("README".to_string(), String::new())
);
}
#[test]
fn test_clean_filename_special_identifiers() {
let config = Config::default();
let sequence = Sequence::default();
// C++ should be preserved
assert_eq!(
clean_filename(OsStr::new("test C++.txt"), &config, &sequence, false),
Some("test_C++.txt".to_string())
);
// C# should be preserved
assert_eq!(
clean_filename(OsStr::new("guide C#.pdf"), &config, &sequence, false),
Some("guide_C#.pdf".to_string())
);
}
#[test]
fn test_clean_filename_no_change_needed() {
let config = Config::default();
let sequence = Sequence::default();
// Already clean filenames should return None
assert_eq!(
clean_filename(OsStr::new("clean_file.txt"), &config, &sequence, false),
None
);
assert_eq!(
clean_filename(OsStr::new("another-file.pdf"), &config, &sequence, false),
None
);
}
#[test]
fn test_clean_filename_empty_after_cleaning() {
let config = Config::default();
let sequence = Sequence::default();
// File with only special chars should become "unnamed"
assert_eq!(
clean_filename(OsStr::new("###.txt"), &config, &sequence, false),
Some("unnamed.txt".to_string())
);
}
#[test]
fn test_clean_filename_apostrophe() {
let config = Config::default();
let sequence = Sequence::default();
// Apostrophes should be removed (not replaced with underscore)
assert_eq!(
clean_filename(OsStr::new("O'Reilly.pdf"), &config, &sequence, false),
Some("OReilly.pdf".to_string())
);
}
}

1
target/.rustc_info.json Normal file
View file

@ -0,0 +1 @@
{"rustc_fingerprint":4740973386762217857,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.90.0 (1159e78c4 2025-09-14)\nbinary: rustc\ncommit-hash: 1159e78c4747b02ef996e55082b704c09b970588\ncommit-date: 2025-09-14\nhost: x86_64-unknown-linux-gnu\nrelease: 1.90.0\nLLVM version: 20.1.8\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/dschlueter/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}

3
target/CACHEDIR.TAG Normal file
View file

@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View file

View file

@ -0,0 +1 @@
291cfff3ad3b4550

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[]","declared_features":"[]","target":6121981589821494128,"profile":3878794443245516162,"path":10602529704205407992,"deps":[[496936660758387454,"walkdir",false,16653160629585727827],[781203651122893512,"itertools",false,4856790396137166786],[2253645963862362999,"glob",false,17901889553727651475],[2751633865096478575,"once_cell",false,404175244902379717],[4861353637455856501,"emojis",false,7812852181959777352],[5957121993870933949,"indicatif",false,6846573194333882574],[6903225003750382070,"env_logger",false,12129958749168747241],[8121005825001993377,"toml",false,14529174445398999363],[8444115378192700076,"anyhow",false,15790217775176775280],[9300925758419628329,"log",false,7601692817919657737],[9722518470958012960,"clap",false,6017947166156778644],[11266840602298992523,"thiserror",false,2303909277957177506],[11641382387439738731,"regex",false,1153832817102413080],[11892580040701647366,"serde",false,16170242140613763432],[16233166307772572446,"unicode_segmentation",false,13473434130131659026],[17775862536196513609,"rayon",false,4741632797656246936],[17902303992486982172,"dirs",false,15255450532072959597]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/NameToUnix-52eb14cd7f851fcf/dep-bin-NameToUnix"}}],"rustflags":[],"metadata":11238891150943416482,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
4dd8cd57cf0e9b09

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":9771195463141993919,"profile":11260102936901317155,"path":16307491102300075524,"deps":[[554324495028472449,"memchr",false,217317700187571089]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/aho-corasick-17f8b4266e41b6ce/dep-lib-aho_corasick"}}],"rustflags":[],"metadata":13904389431191498124,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
16e4bdea9a608b73

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"auto\", \"default\", \"wincon\"]","declared_features":"[\"auto\", \"default\", \"test\", \"wincon\"]","target":1736373845211751465,"profile":5847708262638652504,"path":15382520749374289969,"deps":[[821897733253474908,"anstyle",false,18094389414750515593],[6726333832837302156,"anstyle_query",false,6517661958451464783],[8720183142424604966,"utf8parse",false,18444629462088036825],[9119385831240683871,"is_terminal_polyfill",false,15365458187358078183],[16168342247272166835,"anstyle_parse",false,11337828736863921905],[17599588001959536047,"colorchoice",false,1614739890724786504]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/anstream-c3c9a361c5ec1896/dep-lib-anstream"}}],"rustflags":[],"metadata":7500874485387469444,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
89f1caf54d2f1cfb

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":4691279112367741833,"profile":5847708262638652504,"path":14159642671587559024,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/anstyle-86948fab8bcbae58/dep-lib-anstyle"}}],"rustflags":[],"metadata":14064844656010464607,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
f166fd702a0f589d

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"utf8\"]","declared_features":"[\"core\", \"default\", \"utf8\"]","target":985948777999996156,"profile":5847708262638652504,"path":4210820189472297458,"deps":[[8720183142424604966,"utf8parse",false,18444629462088036825]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/anstyle-parse-23f5e4434e53721e/dep-lib-anstyle_parse"}}],"rustflags":[],"metadata":9799137552285937175,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
4f8a93e70463735a

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[]","declared_features":"[]","target":2663518930196293257,"profile":5847708262638652504,"path":1082465322798495850,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/anstyle-query-537770b39eee8b06/dep-lib-anstyle_query"}}],"rustflags":[],"metadata":12668695791606146315,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
9adcd6582ae11700

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":13708040221295731214,"profile":12092956212200378817,"path":8693219203733942013,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/anyhow-661dc695d4d2effa/dep-build-script-build-script-build"}}],"rustflags":[],"metadata":17154292783084528516,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
70767807681f22db

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":9658695707525313347,"profile":11260102936901317155,"path":16004131161169875048,"deps":[[8444115378192700076,"build_script_build",false,1931718327452350288]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/anyhow-c9b83c3045989c9e/dep-lib-anyhow"}}],"rustflags":[],"metadata":17154292783084528516,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[8444115378192700076,"build_script_build",false,6721496459697306]],"local":[{"RerunIfChanged":{"output":"release/build/anyhow-e9d92686298e2aea/output","paths":["build/probe.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"metadata":0,"config":0,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
94ecd72b100b8453

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"color\", \"default\", \"derive\", \"error-context\", \"help\", \"std\", \"suggestions\", \"usage\"]","declared_features":"[\"cargo\", \"color\", \"debug\", \"default\", \"deprecated\", \"derive\", \"env\", \"error-context\", \"help\", \"std\", \"string\", \"suggestions\", \"unicode\", \"unstable-derive-ui-tests\", \"unstable-doc\", \"unstable-ext\", \"unstable-styles\", \"unstable-v5\", \"usage\", \"wrap_help\"]","target":12724100863246979317,"profile":11126873348164009224,"path":7288412796159717841,"deps":[[4771800536484884094,"clap_derive",false,9333237172541925279],[10543150655979683594,"clap_builder",false,11544520963803449429]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/clap-e9c6ad76aa568e1d/dep-lib-clap"}}],"rustflags":[],"metadata":13636260659328210681,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
55088eada66036a0

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"color\", \"error-context\", \"help\", \"std\", \"suggestions\", \"usage\"]","declared_features":"[\"cargo\", \"color\", \"debug\", \"default\", \"deprecated\", \"env\", \"error-context\", \"help\", \"std\", \"string\", \"suggestions\", \"unicode\", \"unstable-doc\", \"unstable-ext\", \"unstable-styles\", \"unstable-v5\", \"usage\", \"wrap_help\"]","target":4540639333657397710,"profile":11126873348164009224,"path":17035136592378992176,"deps":[[821897733253474908,"anstyle",false,18094389414750515593],[967775003968733193,"strsim",false,4892122836260516362],[2754101768631515696,"anstream",false,8325854554604037142],[3140197793370367388,"clap_lex",false,8909592952030357950]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/clap_builder-de574c5a5b478a16/dep-lib-clap_builder"}}],"rustflags":[],"metadata":13636260659328210681,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
9f27d541c2518681

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\"]","declared_features":"[\"debug\", \"default\", \"deprecated\", \"raw-deprecated\", \"unstable-v5\"]","target":3781261180330156922,"profile":6102714352311999568,"path":11331898117932305525,"deps":[[13033644984628948268,"proc_macro2",false,8300779761218470861],[13203937751714536251,"syn",false,9862373649362752304],[16133888191189175860,"quote",false,3817173719678386389],[17175234422038868540,"heck",false,18199109636904376427]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/clap_derive-bf5a7f3714bb7b87/dep-lib-clap_derive"}}],"rustflags":[],"metadata":9083421305396387959,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
bee917b7953ba57b

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[]","declared_features":"[]","target":5587326852571317598,"profile":11126873348164009224,"path":2020453500475058870,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/clap_lex-9ac9b1caeb1d3c0f/dep-lib-clap_lex"}}],"rustflags":[],"metadata":14823610342382530208,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
48d14d1760b56816

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[]","declared_features":"[]","target":10544268938077819509,"profile":5847708262638652504,"path":17421671175600072335,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/colorchoice-ca24d91229698482/dep-lib-colorchoice"}}],"rustflags":[],"metadata":5376015212253958680,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
5492b3adc5f8f811

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"ansi-parsing\", \"unicode-width\"]","declared_features":"[\"ansi-parsing\", \"default\", \"unicode-width\", \"windows-console-colors\"]","target":8374820256266716131,"profile":11260102936901317155,"path":842659123297130874,"deps":[[2751633865096478575,"once_cell",false,404175244902379717],[4024328380392812020,"unicode_width",false,350253125875139862],[11698369227143406027,"libc",false,12256160239455567883]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/console-3b2c3b25f57d32dc/dep-lib-console"}}],"rustflags":[],"metadata":8886294787439230123,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
ffc01a41260e9eeb

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":8674947694680330387,"profile":10468086882058334769,"path":734875100910984840,"deps":[[13100939403401765317,"crossbeam_utils",false,9951845909837846733],[17638357056475407756,"crossbeam_epoch",false,2548151048078050890]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-deque-fda071f68d178793/dep-lib-crossbeam_deque"}}],"rustflags":[],"metadata":14304628380895324452,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
4a46bba6e0d95c23

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"loom\", \"loom-crate\", \"nightly\", \"std\"]","target":3011025219128477647,"profile":11260102936901317155,"path":17679954352219338837,"deps":[[13100939403401765317,"crossbeam_utils",false,9951845909837846733]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-epoch-0f0085168bed6545/dep-lib-crossbeam_epoch"}}],"rustflags":[],"metadata":8562320424510714295,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

View file

@ -0,0 +1 @@
cd184fbb200f1c8a

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":14378767424822979028,"profile":10468086882058334769,"path":16187828673734176999,"deps":[[13100939403401765317,"build_script_build",false,7972816566730721956]],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-utils-75f633aca295a9b6/dep-lib-crossbeam_utils"}}],"rustflags":[],"metadata":1609393243086812936,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[13100939403401765317,"build_script_build",false,11237325336941604364]],"local":[{"RerunIfChanged":{"output":"release/build/crossbeam-utils-bf70109116b76756/output","paths":["no_atomic.rs"]}}],"rustflags":[],"metadata":0,"config":0,"compile_kind":0}

View file

@ -0,0 +1 @@
{"rustc":2484451964687019519,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"loom\", \"nightly\", \"std\"]","target":9652763411108993936,"profile":1689406044471684405,"path":6340828273201808646,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release/.fingerprint/crossbeam-utils-ddf01c9607bc9735/dep-build-script-build-script-build"}}],"rustflags":[],"metadata":1609393243086812936,"config":2202906307356721367,"compile_kind":0}

View file

@ -0,0 +1 @@
This file has an mtime of when this was started.

Some files were not shown because too many files have changed in this diff Show more