neue Optionen (-r, Installskript) installiert

This commit is contained in:
Dieter Schlüter 2026-02-10 15:38:53 +01:00
commit d78e318d8a
15 changed files with 273 additions and 42 deletions

View file

@ -5,6 +5,32 @@ 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.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

2
Cargo.lock generated
View file

@ -4,7 +4,7 @@ version = 4
[[package]]
name = "NameToUnix"
version = "0.3.0"
version = "1.0.0"
dependencies = [
"anyhow",
"assert_cmd",

View file

@ -1,6 +1,6 @@
[package]
name = "NameToUnix"
version = "0.3.0"
version = "1.0.0"
edition = "2021"
authors = ["Dieter Schlüter <dieter.schlueter@linix.de>"]
description = "Ein Tool zum Anpassen von Verzeichnis- und Dateinamen an Linux-Konventionen"

View file

@ -27,6 +27,18 @@ 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
```
## Functions / Funktionen
- Replaces spaces and special characters in file and directory names with underscores
@ -103,30 +115,36 @@ sudo mandb # Update man database
## Usage
```bash
# Basic usage
# 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 /path/to/files
ntu -n /path/to/files # Short form
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 /path1 /path2 /path3
ntu -r /path1 /path2 /path3
# Exclude specific files
ntu -e "*.tmp" -e "backup_*" /path/to/files
ntu -r -e "*.tmp" -e "backup_*" /path/to/files
# Process symlinks and special files (normally skipped)
ntu --special /path/to/files
ntu -r --special /path/to/files
# Increase verbosity
ntu -v /path/to/files
ntu -r -v /path/to/files
# Also rename the root directory
ntu --modify-root /path/to/files
ntu -r --modify-root /path/to/files
# Combine options
ntu --dry-run -v --special /path/to/files
ntu --dry-run -r -v --special /path/to/files
```
@ -136,30 +154,36 @@ ntu --dry-run -v --special /path/to/files
## Verwendung
```bash
# Grundlegende Verwendung
# 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 /pfad/zu/dateien
ntu -n /pfad/zu/dateien # Kurzform
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 /pfad1 /pfad2 /pfad3
ntu -r /pfad1 /pfad2 /pfad3
# Bestimmte Dateien ausschließen
ntu -e "*.tmp" -e "backup_*" /pfad/zu/dateien
ntu -r -e "*.tmp" -e "backup_*" /pfad/zu/dateien
# Symlinks und Special Files verarbeiten (normalerweise übersprungen)
ntu --special /pfad/zu/dateien
ntu -r --special /pfad/zu/dateien
# Verbosity erhöhen
ntu -v /pfad/zu/dateien
ntu -r -v /pfad/zu/dateien
# Auch das Wurzelverzeichnis umbenennen
ntu --modify-root /pfad/zu/dateien
ntu -r --modify-root /pfad/zu/dateien
# Optionen kombinieren
ntu --dry-run -v --special /pfad/zu/dateien
ntu --dry-run -r -v --special /pfad/zu/dateien
```

View file

@ -7,6 +7,8 @@ _ntu() {
typeset -A opt_args
_arguments -C \
'(-r --recursive)'{-r,--recursive}'[Process directories recursively]' \
'--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]' \
@ -14,6 +16,7 @@ _ntu() {
'(-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'

View file

@ -7,7 +7,7 @@ _ntu_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
# All available options
opts="--dry-run --no-changes --quiet --force --exclude --verbose --modify-root --special --help --version -n -q -f -e -v -h -V"
opts="--recursive --conf --dry-run --no-changes --quiet --force --exclude --verbose --modify-root --special --no-color --help --version -r -n -q -f -e -v -h -V"
# Handle options that require arguments
case "${prev}" in
@ -16,6 +16,11 @@ _ntu_completion() {
COMPREPLY=( $(compgen -W '"*.tmp" "*.log" "*.bak" "*.swp" "*~"' -- ${cur}) )
return 0
;;
--conf)
# Suggest files for config option
COMPREPLY=( $(compgen -f -- ${cur}) )
return 0
;;
*)
;;
esac

View file

@ -4,6 +4,8 @@
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 -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'

View file

@ -1,10 +1,10 @@
# Willkommen bei meinem Projekt
NameToUnix - Rust Command line tool for cleaning up directory & file names according to Linux conventions. Recursively replacing offending characters or spaces.
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 rekursiv und automatisch sinnvoll umzubenennen.
Das Ziel ist es, diese unkonventionellen Dateinamen automatisch sinnvoll umzubenennen. Ab v1.0.0 ist die rekursive Verarbeitung opt-in (`-r` Flag erforderlich).
## Inhaltsverzeichnis

64
install.sh Executable file
View file

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

View file

@ -1,4 +1,4 @@
.TH NTU 1 "2025-02-10" "NameToUnix 0.3.0" "User Commands"
.TH NTU 1 "2025-02-10" "NameToUnix 1.0.0" "User Commands"
.SH NAME
ntu \- sanitize file and directory names to Unix conventions
.SH SYNOPSIS
@ -11,11 +11,19 @@ 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
The tool processes files recursively, starting 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.
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 \-\-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

View file

@ -13,6 +13,14 @@ 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,
/// Ausgaben unterdrücken (keine Umbenennungsinfos auf stdout)
#[clap(short, long)]
pub quiet: bool,

View file

@ -57,6 +57,19 @@ impl Config {
}
}
}
/// 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):

View file

@ -60,9 +60,13 @@ fn main() -> Result<()> {
colored::control::set_override(false);
}
// Optional Konfigurationsdatei laden
let config = Config::from_default_locations(args.verbose)?;
// let config = Config::load(".NameToUnix.conf", args.verbose)?;
// 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
@ -90,7 +94,16 @@ fn main() -> Result<()> {
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)
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))
{

View file

@ -1 +1 @@
{"rustc_fingerprint":18315677830234863098,"outputs":{"8185672236408668984":{"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":""},"11742744481059712885":{"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":{}}
{"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":{}}

View file

@ -30,7 +30,7 @@ fn test_dry_run_no_changes() {
fs::write(&file_path, "test content").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg("--dry-run").arg(temp_dir.path());
cmd.arg("--dry-run").arg("-r").arg(temp_dir.path());
cmd.assert().success();
// File should still have spaces (not renamed)
@ -45,7 +45,7 @@ fn test_actual_rename() {
fs::write(&file_path, "test content").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// File should be renamed
@ -60,7 +60,7 @@ fn test_hidden_files_preserved() {
fs::write(&file_path, "*.tmp").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// Hidden file should not be renamed
@ -74,7 +74,7 @@ fn test_hidden_file_with_spaces() {
fs::write(&file_path, "config content").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// Hidden file should be renamed but keep leading dot
@ -89,7 +89,7 @@ fn test_umlaut_conversion() {
fs::write(&file_path, "test").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// Umlaut should be converted
@ -104,7 +104,7 @@ fn test_double_extension() {
fs::write(&file_path, "archive").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// Should keep .tar.gz intact
@ -123,6 +123,7 @@ fn test_exclude_pattern() {
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg("--exclude")
.arg("*.tmp")
.arg("-r")
.arg(temp_dir.path());
cmd.assert().success();
@ -139,7 +140,7 @@ fn test_quiet_mode() {
fs::write(&file_path, "test").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg("--quiet").arg(temp_dir.path());
cmd.arg("--quiet").arg("-r").arg(temp_dir.path());
cmd.assert()
.success()
.stdout(predicate::str::is_empty());
@ -155,7 +156,7 @@ fn test_multiple_paths() {
fs::write(&file2, "test2").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir1.path()).arg(temp_dir2.path());
cmd.arg("-r").arg(temp_dir1.path()).arg(temp_dir2.path());
cmd.assert().success();
// Both files should be renamed
@ -170,7 +171,7 @@ fn test_parentheses_removed() {
fs::write(&file_path, "test").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// Parentheses should be replaced with underscores
@ -185,10 +186,74 @@ fn test_special_identifiers_preserved() {
fs::write(&file_path, "test").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// C++ should be preserved
assert!(!file_path.exists());
assert!(temp_dir.path().join("C++_Guide.pdf").exists());
}
#[test]
fn test_non_recursive_default() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir(temp_dir.path().join("subdir")).unwrap();
let file1 = temp_dir.path().join("top file.txt");
let file2 = temp_dir.path().join("subdir").join("nested file.txt");
fs::write(&file1, "test1").unwrap();
fs::write(&file2, "test2").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg(temp_dir.path());
cmd.assert().success();
// Top-Level umbenannt
assert!(temp_dir.path().join("top_file.txt").exists());
// Nested NICHT umbenannt (non-recursive)
assert!(file2.exists());
}
#[test]
fn test_recursive_flag() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir(temp_dir.path().join("subdir")).unwrap();
let file1 = temp_dir.path().join("top file.txt");
let file2 = temp_dir.path().join("subdir").join("nested file.txt");
fs::write(&file1, "test1").unwrap();
fs::write(&file2, "test2").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg("-r").arg(temp_dir.path());
cmd.assert().success();
// Beide umbenannt
assert!(temp_dir.path().join("top_file.txt").exists());
assert!(temp_dir.path().join("subdir").join("nested_file.txt").exists());
}
#[test]
fn test_conf_option_valid_file() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("custom.toml");
fs::write(&config_file, "[replacements]\n\"test\" = \"xyz\"").unwrap();
let file_path = temp_dir.path().join("test_file.txt");
fs::write(&file_path, "content").unwrap();
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg("--conf").arg(&config_file).arg(temp_dir.path());
cmd.assert().success();
assert!(temp_dir.path().join("xyz_file.txt").exists());
}
#[test]
fn test_conf_option_missing_file() {
let temp_dir = TempDir::new().unwrap();
let nonexistent = temp_dir.path().join("nonexistent.toml");
let mut cmd = Command::new(cargo_bin!("ntu"));
cmd.arg("--conf").arg(&nonexistent).arg(temp_dir.path());
cmd.assert()
.failure()
.stderr(predicate::str::contains("nicht gefunden"));
}