# fren
**File renamer that understands dates - with slugify, Unicode, and CamelCase smarts**
`fren` is a command-line tool for batch-renaming files and directories. It detects dates inside filenames in 17+ formats, converts them to ISO 8601, slugifies the rest of the name, splits CamelCase, normalizes Unicode, and lowercases extensions. It also includes a separate `merge` command for combining directories with automatic conflict resolution.
## Features
- ๐
**Date Detection**: Recognizes 17+ formats inside filenames (human-readable, ISO, datetime, minute-precision) and rewrites them as ISO 8601
- ๐ค **Slugification**: Cleans filenames by replacing spaces and punctuation with a single separator (default `-`)
- ๐ซ **CamelCase Splitting**: `WhatsApp` becomes `Whats-App`, `JSONFile` becomes `JSON-File`
- ๐ **Unicode Normalization**: Strips accents (`Bancรกrios` -> `Bancarios`) via NFKC + ASCII transliteration
- ๐ **Directory Merging**: A separate `fren merge` command moves files between directories, appending `_Copy` / `_Copy1` / `_Copy2` on conflicts
- ๐ **Exclusions**: Skip specific paths with `-x`
- ๐ **Hidden Files**: Entries starting with `.` are skipped automatically
- ๐ **Dry-run by default**: `fren rename DIR` previews; `--apply` is required to actually rename
- ๐จ **Colored output**: Files in bright green, directories in bright blue, with the unchanged parent path dimmed
- ๐ **Transaction log**: Every applied batch is recorded as JSONL under `${XDG_STATE_HOME:-~/.local/state}/fren/log/`
## Installation
From source:
```bash
git clone https://github.com/andreoliwa/fren.git
cd fren
cargo install --path crates/fren-cli
```
This installs the `fren` binary to `~/.cargo/bin/`.
## Commands
### `fren rename`
Rename files and directories with slugify + ISO date detection.
```bash
fren rename [OPTIONS] DIRECTORIES...
```
Options:
- `-x, --exclude PATH`: Exclude one or more paths (can be repeated)
- `--apply`: Actually perform the renames (without this, `fren` only prints what it would do)
- `--no-log`: Skip writing the transaction log
- `--log-dir DIR`: Override transaction-log directory
Examples:
```bash
# Preview (dry-run is the default)
fren rename ~/Documents/MyFiles
# Actually rename
fren rename --apply ~/Documents/MyFiles
# Multiple directories with exclusions
fren rename --apply -x ~/temp/skip -x ~/temp/important.txt ~/temp
```
### `fren merge`
Merge source directories into a target directory. Move-only - filenames are preserved (with `_Copy` suffixes on conflicts). If you also want to rename the merged contents, run `fren rename` afterwards.
```bash
fren merge [OPTIONS] TARGET SOURCES...
```
Options:
- `--apply`: Actually perform the moves
Examples:
```bash
# Preview
fren merge ~/Documents/Target ~/Documents/Source1 ~/Documents/Source2
# Apply
fren merge --apply ~/Documents/Target ~/Documents/Source1 ~/Documents/Source2
# Merge several into the current directory
fren merge --apply . src1/ src2/ src3/
```
### `fren completions`
Print shell completions.
```bash
fren completions bash > ~/.local/share/bash-completion/completions/fren
fren completions zsh > ~/.zsh/completions/_fren
fren completions fish > ~/.config/fish/completions/fren.fish
```
## How rename works
The pipeline transforms filenames in this order:
1. **Unicode normalize** (NFKC) and transliterate non-ASCII to ASCII
2. **Inject separator at CamelCase boundaries** (`WhatsApp` -> `Whats_App`)
3. **Inject separator at "at"-time patterns** (`2019-08-21 at 14.24.19` -> `2019-08-21_14_24_19`)
4. **Slugify**: replace whitespace and punctuation with the internal separator
5. **Detect dates** and rewrite them as ISO 8601
6. **Apply case mode** (default: preserve original case)
7. **Collapse consecutive separators** and **substitute to user separator** (default `-`)
8. **Lowercase the file extension**
### Examples
```text
Hello World 2024-01-15.txt -> Hello-World-2024-01-15.txt
WhatsApp Image 2024-01-15 at 12.30.45.jpg -> Whats-App-Image-2024-01-15T12-30-45.jpg
CamelCaseFile.PDF -> Camel-Case-File.pdf
Bancรกrios.txt -> Bancarios.txt
report-25-04-2017.pdf -> report-2017-04-25.pdf
photo_20191020.jpg -> photo-2019-10-20.jpg
2026-05-03-18-57.log -> 2026-05-03T18-57-00.log
```
### Supported date formats
- **Human-readable**: `DD_MM_YYYY`, `DD/MM/YYYY`, `DD.MM.YYYY`, `DD-MM-YY`, `DDMMYYYY`, `DDMMYY`, `MM_YYYY`
- **ISO / inverted**: `YYYY-MM-DD`, `YYYY_MM_DD`, `YYYYMMDD`, `YYYY_MM`
- **Datetime (full)**: `DD_MM_YYYY_HH_mm_ss`, `YYYY_MM_DD_HH_mm_ss`, `YYYYMMDDHHmmss`, `YYYYMMDD_HHmmss`, `DD_MM_YY_HH_mm_ss`, `YY_MM_DD_HH_mm_ss`
- **Datetime (minute-precision, zero-second pad)**: `DD_MM_YYYY_HH_mm`, `YYYY_MM_DD_HH_mm`, `DDMMYYYYHHmm`
Two-digit years between `(current_year + 10)` and `99` are interpreted as 19YY; otherwise 20YY. With the system clock at 2026 this means `30..=99` -> 1930..1999 and `00..=29` -> 2000..2029, except dates more than 10 years in the future, which roll back a century.
## How merge works
`fren merge TARGET SOURCES...`:
1. Walks each source recursively
2. Computes the target path = `TARGET / relative_subpath_from_source`
3. If the target file already exists (or another move in the batch already claimed that path), appends `_Copy`, `_Copy1`, `_Copy2`, ... to the stem until a free name is found
4. Creates intermediate directories as needed
5. Moves the file with `std::fs::rename`
6. Skips `.DS_Store` and similar metadata files
Source directory structure is preserved verbatim. Only files are moved; empty source directories remain.
## Architecture
`fren` is a Cargo workspace with three crates:
- **`crates/slug-preserve`** (internal): a case-preserving slugifier. Unlike most Rust slug crates that always lowercase, this one supports five case modes: `Preserve`, `Lower`, `Upper`, `Title`, `Capitalize`.
- **`crates/fren`**: the library. Exposes `slugify_camel_iso`, `plan`, `execute`, `merge_directories`, `unique_file_name`, plus the public type surface (`RenamePlan`, `DetectedDate`, `FrenError`, `ConflictPolicy`, `SlugOpts`, `LogSink`, `JsonlLogSink`, etc.). No CLI dependencies; library discipline lints deny `print_stdout`, `print_stderr`, `panic`, `unwrap_used`, `expect_used`.
- **`crates/fren-cli`**: a thin clap-derive binary that consumes the library.
The library is the source of truth. The binary parses arguments, builds option structs, and formats output. Other Rust projects can embed `fren` directly without spawning a subprocess.
## Safety
- **Dry-run is the default**. `--apply` is required to mutate the filesystem.
- **No silent overwrites**. Every rename pre-checks the target and refuses to proceed if it exists outside the batch (the `Abort` conflict policy).
- **Within-batch collisions are detected at planning time**. If two source paths would rename to the same target, the batch aborts before any I/O happens.
- **Bottom-up execution**. Deeper paths are renamed first, so a directory rename never invalidates the queued paths of its children. This fixes a class of bugs that affected the original Python implementation.
- **Case-only renames** on case-insensitive filesystems (macOS APFS, Windows NTFS) route through a temporary name to avoid silent no-ops.
- **Transaction log**. Every applied batch writes a JSONL file under `${XDG_STATE_HOME:-~/.local/state}/fren/log/<timestamp>-<batch-uuid>.jsonl` so applied changes can be audited or, in the future, undone.
## Development
```bash
cargo build # debug build
cargo test # 48 tests across slugify, planner, executor, merge
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all
cargo install --path crates/fren-cli # install/refresh ~/.cargo/bin/fren
```
Pre-commit:
```bash
prek install
prek run
```
## License
See the project repository for license information.
## Author
W. Augusto Andreoli (andreoliwa@sent.com)