The batch command to rename a file, on a Mac

The phrase batch command carries a specific history. On Windows it means a text file full of commands, saved with a .bat extension, run by double-clicking it, and the line inside it that renames things is ren. Anyone arriving on a Mac with that model in mind hits two walls in the first minute: the command does not exist, and the wildcards behave differently. Both walls have straightforward equivalents. What is worth building is not a one-off line typed into a window, but the same thing a batch file was: a saved file that runs the job again next month without anybody remembering the syntax.

The command being searched for is not installed

Typing which ren on a Mac returns nothing. Typing which rename returns nothing either, on a completely stock system, which surprises people who found a Linux tutorial first. ren is a builtin of the Windows command interpreter and has no Unix counterpart. rename is a Perl script that ships with many Linux distributions and is not part of macOS.

What macOS has instead is mv, which moves a file, and renaming is what moving looks like when the destination is in the same folder.

mv report-draft.pdf report-final.pdf

That is the whole single file case. There is no separate rename verb, and there never was, because in a Unix filesystem a name is an entry in a directory rather than a property of the file. Changing the name and moving the file between folders are the same operation, which is why one command covers both and why moving a file across folders while also renaming it costs nothing extra.

The word batch has a second meaning that matters here, and the rest of this article treats both: renaming many files in one go, and saving the instructions so they can be run again.

Why the Windows wildcard trick fails

On Windows, ren *.txt *.md works. The command receives the two patterns untouched and does the matching itself. The same line on a Mac produces an error before mv is ever reached.

mv *.txt *.md
zsh: no matches found: *.md

The difference is who expands the asterisk. On a Mac the shell expands patterns before the command runs, replacing each one with the list of files it matches. The first pattern becomes a list of existing text files. The second pattern matches nothing, because no .md files exist yet, and the shell stops with an error rather than passing a literal asterisk through. Even in the case where some .md files happen to exist, mv would receive a flat list of names and would move everything into the last one, which is the more dangerous outcome of the two.

That single fact explains most failed attempts to translate a batch file. Patterns are not arguments on a Mac. They are expanded into arguments, and the destination side of a rename cannot be a pattern at all. Something has to compute the new name for each file individually, and that something is a loop.

The loop that replaces the batch line

for f in *.txt; do
  mv -- "$f" "${f%.txt}.md"
done

Three details in that block are doing real work. ${f%.txt} strips the extension from the end of the variable, which is builtin shell syntax rather than a separate tool. The quotes around "$f" keep filenames containing spaces from being split into several arguments, and filenames with spaces are the normal case on a Mac rather than the exception. The -- tells mv that everything after it is a filename, so a file that happens to start with a hyphen is not mistaken for an option.

Skipping the quotes is the single most common way to lose files. A file called Q3 report.txt without quotes arrives at mv as two separate arguments, and the result is either an error or a move to somewhere unintended.

For anything more elaborate than trimming an extension, zsh includes a dedicated tool that is already on the machine and simply not loaded. One line switches it on.

autoload -U zmv
zmv -n '(*)-draft.pdf' '$1-final.pdf'

Parentheses capture parts of the old name and come back as $1 and $2 in the new one, and -n prints what would happen without touching anything.

Turning the command into a file that runs again

This is where the batch file model actually translates well. The Windows habit of saving commands in a file so they can be reused is correct, and macOS supports it directly.

A shell script is a text file that contains one or more UNIX commands. You run a shell script to perform commands you might otherwise enter at the command line. Source: support.apple.com

A file with the commands, a first line naming the shell, and the executable bit set is the whole mechanism.

#!/bin/zsh
#Usage: retitle.sh <old-extension> <new-extension>
for f in *."$1"; do
  [ -e "$f" ] || continue
  mv -n -- "$f" "${f%.$1}.$2"
done

Save that as retitle.sh, then run chmod 755 retitle.sh to make it executable, exactly as Apple's own instructions describe. After that, ./retitle.sh txt md does the job in whatever folder it is run from, and the syntax never has to be recalled again.

The [ -e "$f" ] || continue line handles the case where nothing matches. Without it the loop runs once with the pattern itself as the filename and produces a confusing error.

Making it double-clickable

A .bat file on Windows runs when double-clicked. macOS has the same arrangement under a different extension. A script saved with a .command extension and made executable is recognised by the system as a Terminal shell script and opens a Terminal window when opened from Finder.

This is not folklore. Checking the file type of any filename.command reports the identifier com.apple.terminal.shell-script, and Terminal itself declares that type in its application record with the role Shell. The extension is a real registration rather than a convention.

Two things are worth knowing before relying on it. The script starts in whatever directory the shell defaults to, not in the folder holding the script, so a rename script meant to act on its own folder needs cd "$(dirname "$0")" as its first working line. And a .command file that arrives by download or email carries a quarantine attribute, which produces a warning on first open, so a script written locally behaves differently from the same script sent to a colleague.

For a job that should act on files selected in Finder rather than on a whole folder, the Shortcuts app has a Run Shell Script action that accepts the selection as input, and it appears in the Finder services menu once saved as a Quick Action. That path avoids the extension entirely.

What changes once someone else can run it

A command typed by the person who wrote it carries a lot of unwritten context: which folder they were standing in, what they meant by the pattern, what they would have noticed if the output looked wrong. A saved file carries none of that, and the same line that was safe interactively becomes risky the moment it can be run by accident.

#!/bin/zsh
set -euo pipefail
cd "$(dirname "$0")" || exit 1
[ "$#" -eq 2 ] || { echo "Usage: $0 <old-ext> <new-ext>"; exit 1 }

Four lines cover most of it. set -e stops the script at the first failing command rather than carrying on through a broken state. set -u turns an unset variable into an error instead of an empty string, which is what prevents a mistyped variable name from expanding a path down to the root of the disk. The cd line anchors the script to its own folder, so it cannot act on whatever directory the window happened to be in. The argument check refuses to run at all when the inputs are missing.

There is one more piece of the batch file habit worth keeping. Windows scripts conventionally start with a comment saying what the file does and how to call it, because a folder of .bat files with cryptic names is unreadable six months later. The same applies here, and comments in a shell script are lines beginning with a number sign. A usage line at the top of the file costs one line and answers the question that otherwise requires reading the loop.

Guard rails that belong in every version

The terminal has no undo. A rename that is wrong is wrong permanently, and the file that was overwritten is not in the Trash. Three habits cover almost all of the risk.

Habit How What it catches
Dry run put echo in front of mv wrong pattern, wrong file set
Refuse overwrites mv -n two files landing on one name
Record the old names ls > /tmp/before.txt needing to reverse the change

The second one has a subtlety that deserves attention. On macOS, mv -n does not overwrite an existing destination, and it also does not report a failure. Running it against a name that already exists returns success and moves nothing. A script cannot tell from the exit status whether the file was renamed, which means a loop can silently skip half its work and finish cleanly. Comparing the file count before and after is the practical check.

Running the loop with echo in front is the cheapest of the three and the one worth making automatic. The output is a list of the exact commands that would run, one per file, and reading that list takes seconds compared to reversing a bad rename by hand.

Recording the old names is the habit people skip and later wish they had kept. A list of filenames written to a file before the rename is enough to reconstruct what happened, and pairing it with a second listing taken afterwards makes the difference obvious. Neither file needs to be kept for long. It only has to survive until the result has been checked, which is usually the same minute.

One case sits outside all three. Files inside a folder that syncs to a cloud service are being watched by another process while the rename runs, and a rename there is a delete and a create as far as that service is concerned. Waiting for the sync to settle before and after, rather than renaming mid-upload, avoids the version conflicts that otherwise appear a few minutes later.

Where to keep the finished script

A script that lives in the folder it was written in gets lost. Two conventional homes solve that. A personal script belongs in a bin folder inside the home directory, added to the PATH variable so it can be called by name from anywhere. A tool that every account on the machine should have belongs in /usr/local/bin, which is the writable part of an otherwise protected directory and survives macOS updates.

Once the script is on the PATH, the invocation stops being a path and becomes a word, which is the point at which people actually use it rather than rewriting the loop each time.

The remaining cost is not the syntax. It is the shape of the work: check the folder in one window, type the command in another, switch back to confirm the result, repeat. Counting those switches for a single rename job usually produces a number people find surprising. A file manager with a built-in terminal removes them by keeping the folder and the shell in one place, so the dry run and the check happen without leaving the view. The comparison of the available tools sets out which ones do that and which do not.

What to change first

Take the rename command that gets retyped most often, put it in a file with a shebang, and run chmod 755 on it. That single step converts a command that has to be remembered into one that has to be named. If the checking still costs more time than the renaming, that is a window problem rather than a shell problem, and Atriens is built around closing it.

Frequently asked questions

Why is there no rename command on macOS?

ren is specific to the Windows command interpreter, and the rename utility found in many Linux tutorials is a Perl script that macOS does not ship. Renaming is done with mv, because in a Unix filesystem a name is a directory entry, so renaming and moving are the same operation. Homebrew can install a Perl rename if a script depends on it.

Why does mv *.txt *.md fail on a Mac when ren *.txt *.md works on Windows?

The shell expands both patterns before mv runs. The second pattern matches nothing, since no .md files exist yet, and zsh stops with a no matches found error. Windows passes the patterns to the command unexpanded, which is why the same line works there. A loop that builds each new name separately is the equivalent.

How do you make a rename script run by double-clicking it?

Give the file a .command extension and run chmod 755 on it. macOS registers that extension as a Terminal shell script, so opening it from Finder launches Terminal and runs it. Add cd "$(dirname "$0")" at the top if the script should act on the folder it is stored in.

Is mv -n enough to prevent files being overwritten?

It prevents the overwrite, but it does not announce that it declined. On macOS the command returns success and simply moves nothing when the destination exists, so a script cannot detect the skip from the exit status. Compare the number of files before and after, or run the loop with echo first to see the collisions.

Where should a rename script be saved so it can be run from anywhere?

A personal script goes in a bin folder inside the home directory, with that folder added to the PATH variable in the shell startup file. A script that every account needs goes in /usr/local/bin, which is writable and unaffected by macOS updates. Either way the script becomes a command name rather than a path.

Back to all posts