Readable source
#!/usr/bin/env python3
"""Plan or apply a collision-safe numbered rename. Python 3.9+, no dependencies."""
import argparse,json,uuid
from pathlib import Path
def plan(folder,pattern,prefix,start,digits):
files=sorted((p for p in folder.glob(pattern) if p.is_file() and not p.is_symlink()),key=lambda p:p.name.casefold())
pairs=[(p,folder/(prefix+str(start+i).zfill(digits)+p.suffix.lower())) for i,p in enumerate(files)]
sources={p.resolve() for p in files};seen=set()
for src,dst in pairs:
key=dst.name.casefold()
if key in seen or (dst.exists() and dst.resolve() not in sources): raise ValueError('Destination collision: '+dst.name)
if '/' in dst.name or '\\' in dst.name: raise ValueError('Invalid destination name')
seen.add(key)
return pairs
def apply(pairs):
states=[]
try:
for src,dst in pairs:
temp=src.with_name('.cw-rename-'+uuid.uuid4().hex+src.suffix)
src.rename(temp);states.append([src,dst,temp,'temp'])
for row in states:
src,dst,temp,_=row
if dst.exists(): raise ValueError('A new collision appeared: '+str(dst))
temp.rename(dst);row[3]='target'
except Exception:
for row in reversed(states):
if row[3]=='target':row[1].rename(row[2]);row[3]='temp'
for src,dst,temp,state in reversed(states):temp.rename(src)
raise
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('folder',type=Path);p.add_argument('--pattern',default='*');p.add_argument('--prefix',default='asset-');p.add_argument('--start',type=int,default=1);p.add_argument('--digits',type=int,default=3);p.add_argument('--apply',action='store_true');a=p.parse_args()
if not a.folder.is_dir():p.error('Folder does not exist.')
if any(c in a.prefix for c in '/\\') or a.start<0 or not 1<=a.digits<=12:p.error('Invalid prefix, start or digits.')
try:
pairs=plan(a.folder.resolve(),a.pattern,a.prefix,a.start,a.digits)
print(json.dumps({'mode':'apply' if a.apply else 'dry-run','renames':[{'from':s.name,'to':d.name} for s,d in pairs]},indent=2))
if a.apply:apply(pairs)
except (ValueError,OSError) as e:p.error(str(e))
if __name__=='__main__':main()