Compare commits
No commits in common. "0ba2057514e186e51274bec0e5ed74e7de9b1d93" and "5e272f438702fd2fddb01e082efa9a03dbff4566" have entirely different histories.
0ba2057514
...
5e272f4387
583 changed files with 24 additions and 6443 deletions
|
|
@ -1,28 +0,0 @@
|
||||||
# .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
107
.github/workflows/ci.yml
vendored
|
|
@ -1,107 +0,0 @@
|
||||||
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
66
.github/workflows/release.yml
vendored
|
|
@ -1,66 +0,0 @@
|
||||||
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
45
.gitignore
vendored
|
|
@ -1,29 +1,22 @@
|
||||||
# Rust / Cargo
|
# ---> Rust
|
||||||
/target
|
# Generated by Cargo
|
||||||
**/*.rs.bk
|
# 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
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
# IDEs
|
# RustRover
|
||||||
.idea/
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
.vscode/
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
*.swp
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
*.swo
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
*~
|
#.idea/
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Project-specific
|
|
||||||
info/
|
|
||||||
CLAUDE.md
|
|
||||||
|
|
||||||
# Test artifacts
|
|
||||||
/test/testverzeichnis
|
|
||||||
|
|
||||||
# Temporary files
|
|
||||||
*.tmp
|
|
||||||
*.log
|
|
||||||
*.bak
|
|
||||||
|
|
||||||
|
|
|
||||||
120
CHANGELOG.md
120
CHANGELOG.md
|
|
@ -1,120 +0,0 @@
|
||||||
# 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
136
CONTRIBUTING.md
|
|
@ -1,136 +0,0 @@
|
||||||
# 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
1142
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
70
Cargo.toml
70
Cargo.toml
|
|
@ -1,70 +0,0 @@
|
||||||
[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
21
LICENSE
|
|
@ -1,22 +1,9 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Dieter Schlüter
|
Copyright (c) 2026 dschlueter
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
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:
|
||||||
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
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
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
388
README.md
|
|
@ -1,387 +1,3 @@
|
||||||
# NameToUnix
|
# ntu
|
||||||
|
|
||||||
[](https://github.com/jamulix/NameToUnix/actions)
|
|
||||||
[](https://github.com/jamulix/NameToUnix/releases)
|
|
||||||
[](https://github.com/jamulix/NameToUnix/releases)
|
|
||||||
[](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.
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
#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 "$@"
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# 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
35
index.md
|
|
@ -1,35 +0,0 @@
|
||||||
# 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
64
install.sh
|
|
@ -1,64 +0,0 @@
|
||||||
#!/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
199
man/ntu.1
|
|
@ -1,199 +0,0 @@
|
||||||
.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
742
release.md
|
|
@ -1,742 +0,0 @@
|
||||||
## 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
63
src/cli.rs
|
|
@ -1,63 +0,0 @@
|
||||||
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
157
src/config.rs
|
|
@ -1,157 +0,0 @@
|
||||||
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
326
src/main.rs
|
|
@ -1,326 +0,0 @@
|
||||||
// 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
613
src/sanitizer.rs
|
|
@ -1,613 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
{"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":{}}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
Signature: 8a477f597d28d172789f06886806bc55
|
|
||||||
# This file is a cache directory tag created by cargo.
|
|
||||||
# For information about cache directory tags see https://bford.info/cachedir/
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
291cfff3ad3b4550
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
4dd8cd57cf0e9b09
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
16e4bdea9a608b73
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
89f1caf54d2f1cfb
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
f166fd702a0f589d
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
4f8a93e70463735a
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
9adcd6582ae11700
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
70767807681f22db
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
50b7e88995d7ce1a
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
94ecd72b100b8453
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
55088eada66036a0
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
9f27d541c2518681
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
bee917b7953ba57b
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
48d14d1760b56816
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
5492b3adc5f8f811
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ffc01a41260e9eeb
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
4a46bba6e0d95c23
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
This file has an mtime of when this was started.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
cd184fbb200f1c8a
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
a48a55f76c22a56e
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
0caee625dbfff29b
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"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}
|
|
||||||
Binary file not shown.
|
|
@ -1 +0,0 @@
|
||||||
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
Loading…
Add table
Add a link
Reference in a new issue