Scripts and file automation

Preview a batch of filename changes before applying them

Compare old and new names in a table and check for conflicts. Only an explicit --apply creates copies in a new folder, keeping the originals.

Show contents

This translation was generated by AI. Check the code, units, and numbers against the original. Native-speaker review has not yet been completed for each language. 한국어

Who this is forBeginners who want to rename multiple files but are concerned about incorrect rules or overwriting files

What you need
  • Install Python 3.12 or later and check the version in a terminal with python --version.
  • Extract the example ZIP into a new folder. Do not run the example from inside the ZIP archive.
  • Open a terminal in the folder containing example.py. If the python command is unavailable on Windows, use py. On macOS or Linux, use python3 if your environment requires it.
  • No external packages or accounts are needed. The included files are synthetic data created for this tutorial.

01Start by previewing just three names

In a batch operation, preventing an incorrect rule from being applied to every file matters more than catching a typo in one file. This example validates the entire name list before showing the plan. The default command does not copy files or create folders. After checking the plan, run the operation separately with --apply.

  1. In the extracted folder, check mapping.csv and the three original files inside source_files.
  2. Compare old_name and new_name in mapping.csv. The left column is the current filename; the right column is the name for the new copy.
  3. Run the default command below and check that the three pairs of names connected by arrows are what you intended.
  4. At this stage, check that source_files is unchanged and outputs does not yet exist.
bash
python example.py

02Read the filename mapping

old_namenew_name
메모 초안.txtnote-draft.txt
견적 1.txtquote-001.txt
사진 설명.txtphoto-notes.txt

Each mapping row links one original to one result. Enter old_name exactly as the actual filename appears, including letter case. The entire operation stops if you omit an original or add a name that does not exist. This rule catches mistakes such as using an old mapping after a new file has been added to the folder.

Values in new_name are treated as conflicting even if they differ only in letter case. This prevents report.txt and REPORT.txt from becoming separate results. Leading or trailing spaces, trailing dots, path separators, and Windows reserved names are also rejected. A person must make the final check in the preview that the naming rules fit the task.

03Full code used in this example

make_plan checks all inputs and conflicts. main prints the validated plan, then checks whether --apply was supplied. Even when applying the plan, it copies file contents under new names instead of using rename on the originals, so you can still inspect the starting state.

example.py
"""기본은 미리보기입니다. --apply일 때만 새 이름의 복사본을 만듭니다."""

import argparse
import csv
import re
import shutil
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
INPUT = BASE / "source_files"
MAPPING = BASE / "mapping.csv"
OUTPUT = BASE / "outputs"
RESERVED = {"CON", "PRN", "AUX", "NUL"} | {
    f"{prefix}{number}" for prefix in ("COM", "LPT") for number in range(1, 10)
}


def validate_name(name):
    # 폴더 경로, Windows 예약 이름, 제어 문자 등을 이름으로 받지 않습니다.
    if (not name or name in {".", ".."} or name != name.strip()
            or name.endswith(".") or re.search(r'[<>:"/\\|?*\x00-\x1f]', name)
            or name.split(".")[0].upper() in RESERVED):
        raise ValueError(f"사용할 수 없는 파일명: {name!r}")


def make_plan():
    if (INPUT.is_symlink() or not INPUT.is_dir()
            or getattr(INPUT, "is_junction", lambda: False)()):
        raise ValueError("source_files는 실제 폴더여야 합니다.")
    if MAPPING.is_symlink():
        raise ValueError("mapping.csv 링크는 처리하지 않습니다.")
    sources = list(INPUT.iterdir())
    if any(path.is_symlink() or not path.is_file() for path in sources):
        raise ValueError("source_files에는 일반 파일만 넣으세요.")
    if not sources:
        raise ValueError("source_files에 파일이 없습니다.")
    plan, old_keys, new_keys = [], set(), set()
    with MAPPING.open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream, strict=True)
        if reader.fieldnames != ["old_name", "new_name"]:
            raise ValueError("mapping.csv의 열은 old_name,new_name이어야 합니다.")
        for row in reader:
            if None in row or any(value is None for value in row.values()):
                raise ValueError(f"mapping.csv {reader.line_num}행의 열 수를 확인하세요.")
            old, new = row["old_name"], row["new_name"]
            validate_name(old)
            validate_name(new)
            if old.casefold() in old_keys:
                raise ValueError(f"원본이 중복 지정되었습니다: {old}")
            if new.casefold() in new_keys:
                raise ValueError(f"새 이름 충돌: {new}")
            old_keys.add(old.casefold())
            new_keys.add(new.casefold())
            plan.append((old, new))
    # 한 파일이라도 누락되거나 없는 파일을 지정하면 아무것도 복사하지 않습니다.
    if {old for old, _ in plan} != {path.name for path in sources}:
        raise ValueError("mapping.csv의 old_name은 원본 파일 전체와 정확히 일치해야 합니다.")
    if OUTPUT.exists() or OUTPUT.is_symlink():
        raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
    return plan


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--apply", action="store_true", help="새 이름의 복사본을 outputs/renamed에 만듭니다.")
    args = parser.parse_args()
    plan = make_plan()  # 충돌 검사는 출력 폴더를 만들기 전에 전체에 대해 끝냅니다.
    for old, new in plan:
        print(f"{old} → {new}")
    if not args.apply:
        print(f"미리보기: {len(plan)}개. 파일은 변경되지 않았습니다.")
        print("이름을 확인한 뒤 python example.py --apply 를 실행하세요.")
        return 0

    OUTPUT.mkdir()
    renamed = OUTPUT / "renamed"
    renamed.mkdir()
    for old, new in plan:
        # 이름 변경 대신 복사합니다. xb는 대상이 생겼으면 덮어쓰지 않고 실패합니다.
        with (INPUT / old).open("rb") as source, (renamed / new).open("xb") as target:
            shutil.copyfileobj(source, target)
    with (OUTPUT / "manifest.csv").open("x", encoding="utf-8-sig", newline="") as stream:
        writer = csv.writer(stream)
        writer.writerow(["old_name", "new_name"])
        writer.writerows(plan)
    print(f"완료: {len(plan)}개 복사본 → {renamed}")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError, csv.Error) as error:
        print(f"중지: {error}", file=sys.stderr)
        raise SystemExit(2)

The store_true option in argparse makes preview the default when --apply is omitted. Copy destinations are opened in xb mode to avoid overwriting a file that already exists. The completed mapping is also recorded in manifest.csv so you can trace a new name back to its original name.

04Create copies from the checked plan

After checking the three preview lines, run the following command in the same folder. If you edited mapping.csv after the preview, run the default command again to check the new plan. The apply command does not remember the previous preview; it rereads and validates the mapping at execution time.

bash
python example.py --apply
LocationExpected result
source_files/3 original files kept under their existing names
outputs/renamed/note-draft.txt, quote-001.txt, photo-notes.txt
outputs/manifest.csv3 original → new name mapping rows after the header

The terminal displays “완료: 3개 Copy본” (complete: three copies) and the output folder path. Open each new file to check its contents as well as its name. For example, the practice document number in note-draft.txt should match the number in 메모 초안.txt.

05Create a conflict to check that the script stops

  1. Extract another copy into a new folder. Do not mix error tests with a folder that already contains results.
  2. In mapping.csv, change the second new_name to note-draft.txt, matching the first, and save.
  3. Run python example.py --apply. The message “새 이름 충돌” (new-name collision) should appear.
  4. Check that no outputs folder was created and all three originals remain in source_files.
  5. Restore the original mapping values, then check again from the preview step.

If validation happened one file at a time inside the copy loop, the first file could be processed before the second caused a failure. This example checks all names first so these pre-check errors do not cause partial application. It also stops before starting if outputs already exists.

06Use your own document naming rules

Start with a small copy of a single folder when applying this to your work. Put copies of the originals in source_files and write one row per file in mapping.csv. Use the full name, including the extension. An extension does not convert the file format, so do not map a text file to a name ending in .pdf.

If you add numbers, use a fixed number of digits such as 001 and 002, and use a consistent date order to make results easier to find. Agree on which fields belong in the name first, then check their order and any omissions in the preview. This example does not extract titles from document contents or infer your intent to generate names automatically.

07What to check for each stop message

MessageWhat to checkSolution
New-name collision (새 이름 충돌)Identical new_name values or values that differ only in letter caseChoose distinct final names.
Original specified more than onceold_name repeated in two rowsKeep one row per original.
Must match the complete set of original filesA missing file or a typo in old_nameCompare the folder contents with the mapping again.
Invalid filenamePaths, reserved names, forbidden characters, or trailing dotsEnter only the filename and remove restricted characters.
outputs already existsResults from a previous copy operationRename the result folder to keep it, then start again.

Repeating the same command after applying a plan does not update existing results. Even the preview stops if it finds an existing outputs folder. This rule keeps the results of separate operations from being mixed. No overwrite option is provided.

08Limits of copying and checks that remain

This example only reads the original files and gives the copies new names. Keeping both the originals and their copies requires additional storage. It preserves document contents, but it is not a tool for duplicating creation and modification times, access permissions, or all operating-system metadata.

Errors found before copying, such as name conflicts, stop the entire operation. Once copying starts, a power failure or insufficient disk space is not rolled back as a single transaction. Some files may remain in outputs, so check the completion message and all three files. Concurrent changes to originals or the mapping by another program also require a separate design.

Execution and verification record

2026-09-19 · Windows 11 · CPython 3.12.14 · No additional packages · Run in a temporary copy of the distribution

  • The default preview creates no output folder
  • The 3 copies made with --apply are byte-for-byte identical to the originals
  • Identical-name and case-only conflicts are rejected before output is created
  • Duplicate or omitted originals, nonexistent originals, path traversal, and reserved names are rejected
  • A repeated run stops while preserving existing results
Verification limits
  • The original files themselves are not renamed.
  • Partial output is not automatically rolled back after a disk error.
  • Not all file metadata is preserved.
  • Execution was verified on Windows.

Site-wide writing and verification principles

Example files to run yourself

Includes code, input data, and instructions. Extract the ZIP and read README.txt first.

Download example ZIP

Example code, filenames, and input keys remain unchanged. Refer to the commands and checking steps in the translated article as well.

Practice materials created for this site · Keep your originals separately before running.

References

The explanations and examples were written for this site. See the official sources below for the related behavior and concepts.