runtime
115 / 250
lesson 115 CLI & Automation · intermediate

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.

You'll be able to
  • Build a rename plan as a list of (old, new) pairs without touching the files
  • Offer plan and apply subcommands with add_subparsers
  • Apply changes safely (never overwrite) and report what happened
The idea

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:

  1. 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.
  2. 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.

example-1.py · A rename plan
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.

example-2.py · plan and apply as subcommands
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.

Review
  • Plan first: a list of (old, new) pairs built without touching the files.
  • A dry run prints the plan; apply carries it out and never overwrites; a report says what changed and what was skipped.
  • add_subparsers(dest="command", required=True) gives a tool plan and apply subcommands.
  • Next module: when a program goes wrong, reading tracebacks, finding the cause and logging what happens.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Automate a folder: plan, apply, report — CLI & Automation · Runtime