diff --git a/.gitignore b/.gitignore index f8b73e7..29dba5a 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,3 @@ dmypy.json # Cython debug symbols cython_debug/ - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..bd8fe37 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +--- +default_language_version: + python: python3.8 +repos: + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-added-large-files + - id: check-merge-conflict + - id: debug-statements + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: trailing-whitespace + - id: name-tests-test + exclude: tests/(common.py|util.py|(helpers|integration/factories)/(.+).py) + - repo: https://github.com/asottile/reorder_python_imports + rev: v2.4.0 + hooks: + - id: reorder-python-imports + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.800 + hooks: + - id: mypy diff --git a/README.md b/README.md index fa8f82a..b6fd780 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # paddy-files -Zero-padding numeric filenames \ No newline at end of file +Zero-padding numeric filenames diff --git a/paddy b/paddy new file mode 100755 index 0000000..4e10a92 --- /dev/null +++ b/paddy @@ -0,0 +1,128 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +import argparse +import re +from pathlib import Path +from typing import List +from typing import Optional + + +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() -> 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() + + # Dry run implies verbose + if args.dry_run: + args.verbose = True + + return args + + +def main() -> int: + args = parse_args() + + # Compile ignore pattern, if provided + ignore_matcher: Optional[re.Pattern] = None + if args.ignore: + ignore_matcher = re.compile(args.ignore) + + # Build list of files to act on + p = Path(".") + files = [] + for f in args.files: + if args.ignore_files and f in args.ignore_files: + continue + if ignore_matcher and ignore_matcher.match(f): + continue + files.append(p / f) + + 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 in files: + numbers = extract_numbers(f.name) + if len(numbers) == pad_len: + if args.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 + + # 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}") + f.rename(new_file) + else: + print( + f"Could not ename {f.name} to {new_file.name}. Destination file exists." + ) + status = 1 + + return status + + +if __name__ == "__main__": + exit(main())