Automate a folder: plan, apply, report
A script that renames or moves files should first work out what it would do, show that plan, and only then act. Subcommands give the user both: `plan` to look, `apply` to change.
- Build a rename plan as a list of
(old, new)pairs without touching the files - Offer
planandapplysubcommands withadd_subparsers - Apply changes safely (never overwrite) and report what happened
Scanned invoices arrive with names like Invoice 489434.CSV and Credit Note C489449.csv, and the accounting tool wants invoice-489434.csv. Renaming by hand is slow; renaming with a buggy script can destroy files. Careful automation separates two steps:
- Plan: read the folder and build a list of
(old name, new name)pairs. Nothing on disk changes, so you can print the plan and check it. Printing a plan without acting on it is called a dry run. - Apply: carry out exactly that plan, then report what was done.
Because the plan only lists names that would change, running the tool a second time finds nothing to do. That property (running twice is the same as running once) is what makes a script safe to re-run after a failure.
Run each example to see what it prints. Running examples doesn't count toward progress.
from pathlib import Path def tidy_name(name): return name.strip().lower().replace(" ", "-") def rename_plan(folder): plan = [] for path in sorted(Path(folder).iterdir()): if not path.is_file(): continue new_name = tidy_name(path.name) if new_name != path.name: plan.append((path.name, new_name)) return plan inbox = Path("inbox") inbox.mkdir() for name in ["Invoice 489434.CSV", "invoice-489435.csv", "Credit Note C489449.csv"]: (inbox / name).write_text("Invoice,Quantity\n") for old, new in rename_plan("inbox"): print(old, "=>", new) print(sorted(path.name for path in inbox.iterdir()))
Tools like git have subcommands: git status looks, git commit changes. argparse builds them with add_subparsers: each subcommand gets its own small parser and its own arguments, and dest="command" stores which one was chosen. required=True makes choosing one mandatory.
Path.rename(target) moves a file to a new name. On Linux and macOS it silently replaces a file that already has the target name, so a careful apply step checks target.exists() first and skips (and reports) that pair instead of destroying a file.
import argparse from pathlib import Path def tidy_name(name): return name.strip().lower().replace(" ", "-") def rename_plan(folder): plan = [] for path in sorted(Path(folder).iterdir()): if path.is_file() and tidy_name(path.name) != path.name: plan.append((path.name, tidy_name(path.name))) return plan def apply_plan(folder, plan): renamed = 0 for old, new in plan: target = Path(folder, new) if target.exists(): print("skipped", old, "because", new, "exists") continue Path(folder, old).rename(target) renamed += 1 return renamed def main(argv): parser = argparse.ArgumentParser(prog="tidy") commands = parser.add_subparsers(dest="command", required=True) commands.add_parser("plan").add_argument("folder") commands.add_parser("apply").add_argument("folder") args = parser.parse_args(argv) plan = rename_plan(args.folder) if args.command == "plan": print("planned renames:", len(plan)) for old, new in plan: print(" ", old, "=>", new) else: print("renamed", apply_plan(args.folder, plan), "of", len(plan)) Path("scans").mkdir() for name in ["Invoice 489434.CSV", "invoice-489434.csv", "Credit Note C489449.csv"]: Path("scans", name).write_text("Invoice,Quantity\n") main(["plan", "scans"]) main(["apply", "scans"]) main(["plan", "scans"])
When to use it: any script that changes, moves or deletes many files. Plan first, let people see the plan, then apply.
Mistakes to avoid: renaming while you are still listing the folder (the listing can change under you: build the whole plan first); trusting rename not to overwrite; and finishing silently. A short report of what changed, what was skipped and why is what lets someone trust an automated run.
- Plan first: a list of
(old, new)pairs built without touching the files. - A dry run prints the plan;
applycarries it out and never overwrites; a report says what changed and what was skipped. add_subparsers(dest="command", required=True)gives a toolplanandapplysubcommands.- Next module: when a program goes wrong, reading tracebacks, finding the cause and logging what happens.