Rename files by example: no regex, no GUI, just two before → after names
exform is a free, open-source (MIT), programming-by-example CLI. This is an honest write-up of one of its features.
The problem with bulk rename
Every few weeks you need to rename a pile of files: camera dumps, exported reports, downloaded episodes, scan_0001…scan_0480. The usual options are all a bit painful:
- A GUI batch renamer — powerful, but it means leaving the terminal, clicking through a rules panel, and it's a different app on every OS.
rename/mmv/ aforloop withsed— fast once it works, but you're back to authoring a regex and getting the escaping right for a one-off job.- Do it by hand — fine for five files, miserable for five hundred.
There's a fourth option that spreadsheets have had for years and the terminal somehow never did: just show it what you want.
Rename by example
exform --rename takes filenames on stdin, a couple of old => new examples, infers the transformation, and applies it to the whole batch:
$ ls *.JPG | exform --rename \
-e 'DSC_0001.JPG => 0001.jpg' \
-e 'DSC_0002.JPG => 0002.jpg'
exform: 3 files would be renamed (dry-run). Re-run with --apply to perform the renames.
DSC_0001.JPG -> 0001.jpg
DSC_0002.JPG -> 0002.jpg
DSC_0003.JPG -> 0003.jpg
Notice the third line. I only gave two examples, but DSC_0003.JPG was renamed correctly too — exform inferred "drop the DSC_ prefix and lower-case the extension" rather than memorising those two specific strings. That generalisation is the whole point: two examples, the rest of the batch for free.
It works the same for any consistent pattern — reordering, inserting separators, zero-padding, case changes:
$ ls | exform --rename \
-e 'Track 01 - Intro.mp3 => 01_intro.mp3' \
-e 'Track 02 - Verse.mp3 => 02_verse.mp3'
Track 01 - Intro.mp3 -> 01_intro.mp3
Track 02 - Verse.mp3 -> 02_verse.mp3
Dry-run by default, and it refuses to clobber
Renaming is destructive, so the defaults are conservative on purpose:
- Dry-run by default. You always see the full
old -> newplan first. Nothing touches the disk until you add--apply. - No silent overwrites. If two source files would map to the same new name, or a target already exists, or a source went missing, exform aborts the entire batch and leaves everything untouched. A rename that half-succeeds is worse than one that doesn't run, so it's all-or-nothing.
- Only the basename changes. Directory paths are preserved, so
find . -name '*.jpeg'piped straight in does the right thing per file.
Because it reads paths from stdin, it composes with the tools you already use to select files:
find . -name '*.jpeg' | exform --rename -e 'a.jpeg => a.jpg' --apply
find/fd/ls/git ls-files choose which files; exform decides how to rename them. Unix pipes, not a bespoke rules dialog.
Why "by example" instead of a pattern
Rename is one mode of a small tool with a single idea behind it: describe a text transformation by showing before → after pairs, and let the machine write the program. This is programming-by-example — the same idea as spreadsheet Flash Fill — as an ordinary Unix filter.
The part that matters most is that it's not a black box. exform infers a small, readable program and will show it to you:
$ echo | exform -e 'John Smith => Smith, J.' -e 'Grace Hopper => Hopper, G.' --dry-run
program: field(ws,1) + ', ' + line.first + '.'
You can read that line, sanity-check it, and trust it before piping ten million records through it. It's deterministic and runs entirely offline — no model, no API key, no data leaving your machine — so the same examples always produce the same program. If the examples are ambiguous (e.g. it had to hardcode a literal from your sample), it warns you and asks for another example rather than quietly guessing wrong.
Try it / install
There's a browser playground (the engine compiles to WebAssembly, so it runs entirely client-side — no install, nothing uploaded):
▶ Try exform in your browser ★ Source on GitHub
To install:
pipx install exform # or: pip install exform / uvx exform
Or grab the single-file zero-install build from the releases page and run it with just python3 exform.pyz … — no pip, no virtualenv, no dependencies.
The engine is pure-Python standard-library, zero third-party dependencies, MIT-licensed. Source, issues, and a fuller cookbook (CSV/TSV column edits, in-line "sed-by-example", JSONL field reshaping, and code generation) are on GitHub. If you try it on a real rename job and it guesses wrong, that's a genuinely useful bug report — open an issue with the two example names and what you expected.