You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
155 lines
4.0 KiB
155 lines
4.0 KiB
#! /usr/bin/env python3 |
|
# -*- coding: utf-8 -*- |
|
import argparse |
|
import re |
|
from pathlib import Path |
|
from typing import Generator |
|
from typing import List |
|
from typing import Optional |
|
from typing import Tuple |
|
|
|
|
|
num_matcher = re.compile(r"([0-9]+)") |
|
|
|
|
|
def extract_numbers(s: str) -> str: |
|
number_match = num_matcher.search(s) |
|
if number_match: |
|
return number_match.group(1) |
|
return "" |
|
|
|
|
|
def calc_pad_length(files: List[Path]) -> int: |
|
max_len = 0 |
|
for f in files: |
|
numbers = extract_numbers(f.name) |
|
max_len = max(len(numbers), max_len) |
|
return max_len |
|
|
|
|
|
def parse_args(sys_args: Optional[List[str]] = None) -> argparse.Namespace: |
|
parser = argparse.ArgumentParser( |
|
description="Pads numbers in file names so they consistently align and sort" |
|
) |
|
parser.add_argument("files", nargs="+", metavar="file", help="Files to be renamed") |
|
parser.add_argument( |
|
"-l", |
|
"--length", |
|
type=int, |
|
help="Length of numbers after padding (default: auto)", |
|
) |
|
parser.add_argument( |
|
"-f", |
|
"--force", |
|
action="store_true", |
|
help="Force rename, even if file at destination exists", |
|
) |
|
parser.add_argument( |
|
"-v", "--verbose", action="store_true", help="Print all actions" |
|
) |
|
parser.add_argument( |
|
"-d", |
|
"--dry-run", |
|
action="store_true", |
|
help="Print actions only without modifying any file. Implies --verbose", |
|
) |
|
parser.add_argument( |
|
"-i", |
|
"--ignore", |
|
metavar="REGEX", |
|
help="Regular expression used to ignore files matching the name", |
|
) |
|
parser.add_argument( |
|
"--ignore-files", |
|
nargs="+", |
|
metavar="IGNOREFILE", |
|
help="Files to ignore for renaming. Must add -- before positional arguments", |
|
) |
|
args = parser.parse_args(sys_args) |
|
|
|
# Dry run implies verbose |
|
if args.dry_run: |
|
args.verbose = True |
|
|
|
return args |
|
|
|
|
|
def get_files( |
|
filenames: List[str], |
|
ignore_filenames: List[str], |
|
ignore_pattern: str, |
|
) -> List[Path]: |
|
# Compile ignore pattern, if provided |
|
ignore_matcher: Optional[re.Pattern] = None |
|
if ignore_pattern: |
|
ignore_matcher = re.compile(ignore_pattern) |
|
|
|
p = Path(".") |
|
files: List[Path] = [] |
|
for f in filenames: |
|
if ignore_filenames and f in ignore_filenames: |
|
continue |
|
if ignore_matcher and ignore_matcher.match(f): |
|
continue |
|
files.append(p / f) |
|
|
|
return files |
|
|
|
|
|
def pad_files( |
|
files: List[Path], |
|
pad_len: int, |
|
verbose=False, |
|
) -> Generator[Tuple[Path, Path], None, None]: |
|
for f in files: |
|
numbers = extract_numbers(f.name) |
|
if len(numbers) == pad_len: |
|
if verbose: |
|
print(f"{f.name} is already padded.") |
|
continue |
|
|
|
# Pad number and get destination path |
|
new_numbers = numbers.zfill(pad_len) |
|
new_name = num_matcher.sub(new_numbers, f.name, count=1) |
|
new_file = f.parent / new_name |
|
|
|
if f == new_file: |
|
if verbose: |
|
print(f"{f.name} already matches destination.") |
|
continue |
|
|
|
yield f, new_file |
|
|
|
|
|
def main(sys_args: Optional[List[str]] = None) -> int: |
|
args = parse_args(sys_args) |
|
|
|
# Build list of files to act on |
|
files = get_files(args.files, args.ignore_files, args.ignore) |
|
|
|
pad_len = args.length |
|
if pad_len is None: |
|
pad_len = calc_pad_length(files) |
|
|
|
if args.verbose: |
|
print(f"Padding to {pad_len}") |
|
|
|
status = 0 |
|
for f, new_file in pad_files(files, pad_len, args.verbose): |
|
# Possibly rename unless exists or forced |
|
if not new_file.exists() or args.force: |
|
if args.verbose: |
|
print(f"Rename {f.name} to {new_file.name}") |
|
if not args.dry_run: |
|
f.rename(new_file) |
|
else: |
|
print( |
|
f"Could not rename {f.name} to {new_file.name}. Destination file exists." |
|
) |
|
status = 1 |
|
|
|
return status |
|
|
|
|
|
if __name__ == "__main__": |
|
exit(main())
|
|
|