
Renaming hundreds or thousands of files by hand is slow and error-prone. A rented Mac gives you a persistent macOS desktop with Finder and Terminal; install Python 3 before using the preview script, so you can stage the files, review a plan, and apply the change once. This guide uses Finder's Rename Items panel for the simple cases, shows the Terminal pattern for scripted renames, and ships a preview-only Python 3 manifest that writes a reviewed plan and apply script — it never renames anything on its own.
Batch renaming is light work: the main planning constraint is usually the disk space needed for a working copy. Figment's virtual Intel macOS plan at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) is the useful starting plan for this job. It runs a macOS desktop with Finder and Terminal; install Python 3 separately for the script below, then you reach it through Figment's browser console. Compatible Intel Xcode (Xcode 26 or earlier) also runs here if the renamed files later feed a signed build — Apple's own porting guide says to use Xcode 26 or earlier when running Xcode on an Intel-based processor. Signing supported targets on Intel does not by itself require an Apple-silicon Mac; Apple silicon is required to execute ARM-only binaries, to run Xcode 27, and for visionOS.
If you need ARM execution, Xcode 27, GPU work or more cores, Figment's M-series plans start at $95/month, with the exact chip, RAM and disk confirmed in the quote. The $249 Managed Build Mac adds CI setup, backups, monitoring and 30 minutes of monthly support on one M-series Mac. Physical Intel and M-series hardware is request-based while supply is confirmed, so open a configuration request before you plan a cutover. Billing is monthly in advance; there is no daily pro-rate on the $29 plan, so a two-week job still costs one month. If you run one rename batch per week and leave the Mac up for review and ad-hoc work, $29 divided across roughly four batches is about $7.25 per batch — useful as an upper bound, not a per-run invoice.
Getting the files onto the Mac is not a drag-and-drop over the browser console — the console is a desktop viewer, not a file-transfer channel for arbitrary local files. Use authorized outbound connections from the VM to pull the source files from your own artifact store, Git host, object storage, SFTP endpoint or signed download URL. That is the path to design the workflow around: pull a working copy, keep the originals untouched at the source, and only operate on the copy. Document the exact pull command and credentials source before you start; that pre-rename copy is also what you rely on for recovery.
| Approach | Best for | Install |
|---|---|---|
| Finder Rename Items panel | Simple find/replace, prefixes, numbered formats | Built in |
Terminal for loop with mv -i | Pattern renames, extension changes, zero-padded counters | Built in |
| Python 3 manifest (below) | Any batch over ~20 files, or anything with collision risk | Install Python 3 and check python3 --version |
Finder Rename Items. Select the files in Finder, Control-click the selection, and choose Rename N Items…. The panel offers three modes: Replace Text (find/replace across names), Add Text (prefix or suffix), and Format (name plus a counter, index or date). ⌘Z undoes the batch if you catch a mistake in the same Finder session. Finder does not show a collision preview, so reserve it for small, homogenous selections you can verify by eye.
Naming conventions. Before you run anything, pick and write down a target shape: lowercase vs. title case, separator (_ vs. -), date format (YYYYMMDD), zero-padding width, and whether extensions are normalised (.jpeg → .jpg). A manifest only tells you whether the mapping is consistent; it cannot tell you whether the convention is the one you wanted.
Terminal for loop. For scripted renames, a shell loop works, but use mv -i or mv -n so an unrelated target cannot silently overwrite, and quote every variable:
# Add a prefix to all JPEGs in the current directory
for f in *.jpg; do mv -n -- "$f" "product_$f"; done
# Change extension from .jpeg to .jpg
for f in *.jpeg; do mv -n -- "$f" "${f%.jpeg}.jpg"; done
This still does not catch two source names that would rename to the same target, and macOS's default APFS volume is case-insensitive, so Report.PDF and report.pdf collide even though they look distinct. That is what the manifest below exists for. Homebrew's rename is a fine Perl-flavoured one-liner for already-reviewed renames once the plan is approved; this guide intentionally does not invoke user-supplied Perl against real files as the preview step.
This is the reader tool for the guide. The script does not rename anything. It reads a directory, applies a Python regular-expression substitution in memory, and writes two artifacts:
rename-manifest-<timestamp>.tsv — one row per file with OLD, NEW, STATUSrename-apply-<timestamp>.sh — a shell script with one mv -n -- per approved row, emitted only when the plan has zero rejected rowsRequires Python 3 (3.9+), which is available on macOS. Treat the code as reference: copy it to the Mac, run it against a copy of your data, read both artifacts, then decide whether to apply.
#!/usr/bin/env python3
"""rename_manifest.py - preview-only rename planner (no renames performed).
Usage:
python3 rename_manifest.py <directory> <python-regex> <replacement>
Example:
python3 rename_manifest.py ./copy '^IMG_(\\d+)' 'Photo_\\1'
"""
from __future__ import annotations
import re, sys, time
import shlex
from pathlib import Path
BAD_CHARS = set("/\x00") # path separator and NUL are never valid in a name
STAMP = time.strftime("%Y%m%d-%H%M%S")
def plan(directory: Path, pattern: str, replacement: str) -> list[tuple[str, str, str]]:
rx = re.compile(pattern)
rows: list[tuple[str, str, str]] = []
seen_ci: dict[str, str] = {} # case-folded target -> first OLD name that claimed it
for entry in sorted(p.name for p in directory.iterdir() if p.is_file()):
new = rx.sub(replacement, entry)
if new == entry:
rows.append((entry, new, "SKIP_NO_MATCH")); continue
if not new or new in (".", ".."):
rows.append((entry, new, "REJECT_EMPTY_OR_DOT")); continue
if any(c in BAD_CHARS for c in new):
rows.append((entry, new, "REJECT_PATH_SEPARATOR_OR_NUL")); continue
key = new.casefold() # macOS default FS is case-insensitive
if key in seen_ci:
rows.append((entry, new, f"REJECT_COLLISION_WITH:{seen_ci[key]}"));continue
target = directory / new
if target.exists() and target.name != entry:
rows.append((entry, new, "REJECT_TARGET_EXISTS")); continue
seen_ci[key] = entry
rows.append((entry, new, "OK"))
return rows
def write_artifacts(directory: Path, rows: list[tuple[str, str, str]]) -> None:
manifest = Path(f"rename-manifest-{STAMP}.tsv")
with manifest.open("w", encoding="utf-8") as fh:
fh.write("OLD\tNEW\tSTATUS\n")
for old, new, status in rows:
fh.write(f"{old}\t{new}\t{status}\n")
rejected = [r for r in rows if r[2].startswith("REJECT")]
ok = [r for r in rows if r[2] == "OK"]
print(f"Manifest : {manifest}")
print(f"OK rows : {len(ok)}")
print(f"Rejected : {len(rejected)}")
if rejected:
print("No apply script written. Resolve rejections, then re-run.")
sys.exit(1)
apply_path = Path(f"rename-apply-{STAMP}.sh")
with apply_path.open("w", encoding="utf-8") as fh:
fh.write("#!/usr/bin/env bash\nset -euo pipefail\n")
fh.write(f"cd -- {shlex.quote(str(directory))}\n")
for old, new, _ in ok:
# mv -n refuses to overwrite; shell-quote both sides.
fh.write(f"mv -n -- {shlex.quote(old)} {shlex.quote(new)}\n")
apply_path.chmod(0o755)
print(f"Apply : {apply_path} # review before running")
if __name__ == "__main__":
if len(sys.argv) != 4:
print(__doc__); sys.exit(2)
directory = Path(sys.argv[1]).resolve()
write_artifacts(directory, plan(directory, sys.argv[2], sys.argv[3]))
What the planner refuses to approve, and why it matters:
| Rejection | What it prevents |
|---|---|
REJECT_COLLISION_WITH:<name> | Two source files renaming to the same target, including case-only differences on APFS case-insensitive volumes |
REJECT_TARGET_EXISTS | Overwriting an unrelated file already on disk at the new name |
REJECT_PATH_SEPARATOR_OR_NUL | A substitution that would accidentally create a subdirectory or an invalid name |
REJECT_EMPTY_OR_DOT | A pattern that produces "", "." or ".." |
The apply script is only written when every row is OK or SKIP_NO_MATCH. The apply script itself uses mv -n, which refuses to overwrite an existing target even if the filesystem state changes between review and apply. There is no automatic rollback: once mv has moved part of a batch, a later failure cannot be safely undone by running the moves in reverse — intermediate names may already collide. If you need undo, keep the pre-rename source copy you pulled at the start, and re-pull on failure. That is the reliable recovery path. The script has not been test-executed inside this article; run it against disposable sample data before trusting it on real files.
Work through this list against a copy of your files before touching production:
python3 rename_manifest.py ./copy '<regex>' '<replacement>' and open the generated TSVNEW and scan for anything in STATUS other than OK or SKIP_NO_MATCHOLD/NEW pairs end to end; does the new name make sense to a human?rename-apply-*.sh and read every line; confirm the line count matches the OK row countIf the apply exits part-way, do not try to reverse the mv calls manually — re-pull the pre-rename working copy and re-run the planner. Finder's Undo (⌘Z) only covers renames done through Finder in the current session; it does not undo mv from Terminal. The $29 virtual Intel macOS plan does not include automatic backups; if your workload needs managed backups and monitoring instead of a self-managed source copy, the $249 Managed Build Mac plan includes those on one M-series Mac. Inbound SSH is not configured on the $29 plan; outbound connections from the VM to your artifact store, Git host or CI control plane work normally, which is why the pull-a-copy pattern fits.
Ready to start? Open Mac Hosting, request the $29 virtual Intel macOS plan for a rename-only workload (or an M-series plan if the later build needs ARM execution or Xcode 27), and sign in through the browser console. Pull your working copy over an authorized outbound channel, run the planner, read the TSV, then apply.
Related guides: