Skip to content
de

How to batch rename files on a Mac without overwriting any

A one-line mv loop turned three test photos into one. Why batch renames lose files, what to type instead, and how Namesmith's two-pass rename works, limits included.

3 Nov 2026 · T. Krause · 6 min read

To batch rename files on a Mac without overwriting any, two things have to hold. A rename has to fail when the new name is taken, and names that depend on each other have to pass through a temporary name. A plain mv loop does neither. I ran this one on three test files under macOS 27.2, and it left a single file:

for i in 1 2 3; do mv IMG_$i.jpg IMG_$((i+1)).jpg; done

IMG_1.jpg replaced IMG_2.jpg, that file replaced IMG_3.jpg, and the survivor became IMG_4.jpg, holding the contents of the first photo. mv printed nothing, and the two lost files don't go to the Trash.

What works in Terminal

mv -n refuses to replace an existing file, but it refuses quietly. On the same Mac it skipped the file and still exited with status 0, so && and set -e won't catch a skip. The loop above with -n ended with IMG_1.jpg, IMG_2.jpg and IMG_4.jpg. Nothing was lost, the numbering was wrong and no message said so.

For a numbered series, the order fixes it. Start with the highest number, so each file moves into a name that's already free:

for i in 3 2 1; do mv -n IMG_$i.jpg IMG_$((i+1)).jpg; done

A swap has no safe order, because whichever file moves first lands on the other one. It needs a third name:

mv -n a.txt swap.tmp
mv -n b.txt a.txt
mv -n swap.tmp b.txt

Any closed loop has the same problem, such as a to b, b to c and c to a. Past a handful of files, working out the order by hand is where mistakes happen.

Case changes have a trap of their own. On a default Mac volume, mv -n Hello.txt hello.txt did nothing and exited with 0, while plain mv Hello.txt hello.txt worked.

Two passes through a temporary name

Namesmith is our batch renamer for the Mac. For this article I read its rename code in Renamer.swift and its preview code in Engine.swift, and ran both in a small test program. A batch takes two passes, or two for each folder depth when its items sit at different depths. The first moves each file to a hidden, unique name in the same folder. The second moves each hidden name to its final name:

pass 1   a.txt                      -> .fastrenamer-8F3A0C21-B7D
         b.txt                      -> .fastrenamer-C21D94E0-7A1
pass 2   .fastrenamer-8F3A0C21-B7D  -> b.txt
         .fastrenamer-C21D94E0-7A1  -> a.txt

The random parts above are made up. Each run generates its own from the first 12 characters of a UUID, and .fastrenamer is left over from the app's working name. After the first pass all the old names are free, so the order stops mattering. In the test, a swap, a three-way loop and a numbered shift all put the right contents under the right names, and undo restored each one.

Folders add one rule. Renaming a folder changes the path of everything inside it, so Namesmith starts with the deepest paths and runs both passes for one depth before moving up to the next. The project's own test renames a file, its folder and that folder's parent in one batch, and A/B/f.txt ends up as A2/B2/g.txt. The undo record then rewrites the earlier entries to the new paths, so undo can find them.

The call that refuses to overwrite

Both passes use renamex_np with the flag RENAME_EXCL. According to man 2 rename, it returns EEXIST when the destination already exists. The check happens inside the rename call. Checking first and renaming second would leave a gap in which another program could create that file. In the test, renaming a.txt onto an existing b.txt failed with "b.txt already exists" and both files kept their contents.

The same flag has a limit that I only found by testing. The man page says "on file systems that support it", and exFAT and FAT don't appear to. On disk images in both formats, under macOS 27.2, every rename stopped with "Operation not supported". Nothing was lost and nothing was renamed either. Namesmith's code has no fallback for this, so today it can't rename files directly on a USB stick or camera card in those formats. Copy the files to the Mac's own drive, rename them there and copy them back. APFS worked in both its case-insensitive and case-sensitive variants. I didn't test network shares.

Deciding the names before anything moves

The preview in Engine.swift recalculates as you edit, and again against the current disk state right before a rename. A function of about 70 lines, resolve, decides which names count as taken.

Names held by files in the batch don't count, because those files are about to move. That's why a swap isn't flagged as a clash. Names held by other files in the folder do count.

Two names count as the same if they match after Unicode normalisation and lowercasing. That follows the file system. Apple's APFS FAQ says case-insensitive is the default on macOS and that normalization variants of a name can't exist in the same folder. So Bild.jpg and BILD.jpg in one batch become Bild.jpg and BILD (2).jpg. Namesmith does this on case-sensitive volumes too, where both names could exist, so it's stricter there than it has to be.

Chains are harder. Take a.txt to b.txt, b.txt to c.txt and c.txt to d.txt, with a d.txt outside the batch. With the default setting, "Add a number", the last file becomes d (2).txt. With "Don't rename", c.txt stays put, so its name stays taken, so b.txt has nowhere to go, and neither does a.txt. The resolver starts over each time it drops a file, and it stops when nothing changes. In the test all three rows came back as clashes.

If it dies during a batch

Namesmith writes the undo history after the whole batch finishes, and nothing on disk records the original names while it runs. So a kill at any point in the run can strand files. In the test program I stopped a batch of 60,000 small files with kill -9 after 0.8 seconds, during the first pass. 2,320 files sat under hidden .fastrenamer- names and 57,680 still had their original names. A kill during the second pass left 53,422 hidden and 6,578 under their new names.

Every file's contents were still there, since a rename only changes the folder entry. The hidden files' original names are gone, though, the run has no undo entry, and I found no code that recovers those files at the next launch. They can also sit in subfolders. In a test across three folder levels, the kill left the deepest level finished, the middle one in its first pass and the top one untouched. find . -name '.fastrenamer-*' finds them at every level. Writing a record before the first pass would close this gap. Showpiece does something similar for recordings by saving its state every five seconds.

The run itself is short. 10,000 empty files took 1.45 seconds through both passes on an M3 Pro, and 1,000 took 0.13 seconds. The 60,000 test files took 11 seconds when nothing stopped them.

Once a batch has finished, undo covers the last 50 batches and survives a restart. It runs through the same two passes, so undoing a swap works too. If your real job is keeping two folders alike rather than renaming, the risks are different, and comparing two folders on a Mac covers them.

Try this on a folder where a clash is likely, such as a numbered photo series you want to shift by one. Build the steps in the Namesmith demo and watch which rows turn to "numbered" or "clash". The demo has no time limit and shows the full preview, and --dry-run works on the command line, while the rename itself needs a licence. Namesmith needs macOS 14 or later on Apple silicon. The demo is on the Namesmith page.

All articles

Cookies

This site needs no cookies to work. With your consent it loads Google Analytics to count visits.

Privacy policy