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.
- 📅 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:
WhatsAppbecomesWhats-App,JSONFilebecomesJSON-File - 🌍 Unicode Normalization: Strips accents (
Bancários->Bancarios) via NFKC + ASCII transliteration - 📁 Directory Merging: A separate
fren mergecommand moves files between directories, appending_Copy/_Copy1/_Copy2on conflicts - 🔍 Exclusions: Skip specific paths with
-x - 🙈 Hidden Files: Entries starting with
.are skipped automatically - 🔒 Dry-run by default:
fren rename DIRpreviews;--applyis 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/
From source:
git clone https://github.com/andreoliwa/fren.git
cd fren
cargo install --path crates/fren-cliThis installs the fren binary to ~/.cargo/bin/.
Rename files and directories with slugify + ISO date detection.
fren rename [OPTIONS] DIRECTORIES...Options:
-x, --exclude PATH: Exclude one or more paths (can be repeated)--apply: Actually perform the renames (without this,frenonly prints what it would do)--no-log: Skip writing the transaction log--log-dir DIR: Override transaction-log directory
Examples:
# 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 ~/tempMerge 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.
fren merge [OPTIONS] TARGET SOURCES...Options:
--apply: Actually perform the moves
Examples:
# 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/Print shell completions.
fren completions bash > ~/.local/share/bash-completion/completions/fren
fren completions zsh > ~/.zsh/completions/_fren
fren completions fish > ~/.config/fish/completions/fren.fishThe pipeline transforms filenames in this order:
- Unicode normalize (NFKC) and transliterate non-ASCII to ASCII
- Inject separator at CamelCase boundaries (
WhatsApp->Whats_App) - Inject separator at "at"-time patterns (
2019-08-21 at 14.24.19->2019-08-21_14_24_19) - Slugify: replace whitespace and punctuation with the internal separator
- Detect dates and rewrite them as ISO 8601
- Apply case mode (default: preserve original case)
- Collapse consecutive separators and substitute to user separator (default
-) - Lowercase the file extension
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
- 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.
fren merge TARGET SOURCES...:
- Walks each source recursively
- Computes the target path =
TARGET / relative_subpath_from_source - 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 - Creates intermediate directories as needed
- Moves the file with
std::fs::rename - Skips
.DS_Storeand similar metadata files
Source directory structure is preserved verbatim. Only files are moved; empty source directories remain.
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. Exposesslugify_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 denyprint_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.
- Dry-run is the default.
--applyis 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
Abortconflict 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>.jsonlso applied changes can be audited or, in the future, undone.
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/frenPre-commit:
prek install
prek runSee the project repository for license information.
W. Augusto Andreoli (andreoliwa@sent.com)