#!/usr/bin/env python3
# ------------------------------------------------------------------------------
# rename.py
# =========
#
# Scope Native
# Copyright (C) 2024 by RaySoft, Zurich, Switzerland
# License GNU General Public License (GPL) 2.0
# https://www.gnu.org/licenses/gpl2.txt
#
# ------------------------------------------------------------------------------
DEFAULT_EXT = 'jpg'
BACKUP_EXT = 'bkp'
# ------------------------------------------------------------------------------
PROGRAM_NAME = 'rename'
PROGRAM_VERSION = '0.1'
# ------------------------------------------------------------------------------
from argparse import ArgumentParser
from locale import setlocale, strxfrm, LC_COLLATE
from os import getcwd, listdir
from os.path import basename, isdir, isfile
from random import SystemRandom
from re import compile, search, IGNORECASE
from shutil import get_terminal_size, move
from sys import exit, stderr
# ------------------------------------------------------------------------------
def main():
parser = ArgumentParser(
description='Rename all files with a given extension in a directory.',
prog=PROGRAM_NAME,
)
parser.add_argument(
'-V', '--version', action='version',
version=f'%(prog)s {PROGRAM_VERSION}',
)
group1 = parser.add_mutually_exclusive_group()
group1.add_argument(
'-q', '--quiet', action='store_true',
help='increase verbosity',
)
group1.add_argument(
'-v', '--verbose', action='store_true',
help='suppress non-error messages',
)
parser.add_argument(
'-r', '--random', action='store_true',
help='sort files randomly',
)
parser.add_argument(
'-p', '--path', metavar='PATH', dest='path',
action='store', default=getcwd(),
help="proceed in path (default is '.')",
)
group2 = parser.add_mutually_exclusive_group()
group2.add_argument(
'--all', action='store_true',
help='rename all files',
)
group2.add_argument(
'--ignore', action='store_true',
help='do not rename files which have already the correct '
'prefix and separator (default)',
)
parser.add_argument(
'--prefix', metavar='PRE', dest='prefix', action='store',
help='use file prefix (default is the basename of PATH)',
)
parser.add_argument(
'--separator', metavar='SEP', dest='separator',
action='store', default='-',
help='use separator between prefix and file number '
"(default is '-')",
)
group3 = parser.add_mutually_exclusive_group()
group3.add_argument(
'--extension', metavar='EXT', dest='extension',
action='store', default=DEFAULT_EXT,
help=f"define file extension (default is '{DEFAULT_EXT}')",
)
group3.add_argument(
'--pattern', metavar='PAT', dest='pattern', action='store',
help='use case insensitive regex pattern to filter the files '
"(default is '\\.EXT$')",
)
args = parser.parse_args()
if args.all is None:
args.ignore = True
if args.pattern is None:
args.pattern = f'\\.{args.extension}$'
if args.prefix is None:
args.prefix = basename(args.path)
if not isdir(args.path):
print(f"Error finding directory '{args.path}'!", file=stderr)
parser.print_usage(file=stderr)
return 1
file_index = 0
file_counter = 0
move_counter = 0
progressbar = False
files_2_backup = []
files_2_ignore = []
files_2_process = list(
filter(
lambda file: search(args.pattern, file, flags=IGNORECASE),
listdir(args.path),
)
)
amount_files = len(files_2_process)
amount_files_2_ignore = 0
amount_files_2_process = amount_files
setlocale(LC_COLLATE, 'C')
if amount_files <= 0:
print(f"Error finding files with pattern '{args.pattern}' in "
f"'{args.path}'!", file=stderr)
return 1
if not args.quiet:
print(f"{amount_files} files with extension '{args.pattern}' found in "
f"'{args.path}'.")
if args.ignore:
position = 0
pattern = compile(rf'^{args.prefix}{args.separator}')
while position < len(files_2_process):
if pattern.match(r'{!s}'.format(files_2_process[position])):
files_2_ignore.append(files_2_process.pop(position))
else:
position += 1
amount_files_2_process = len(files_2_process)
amount_files_2_ignore = len(files_2_ignore)
if amount_files_2_ignore > 0:
files_2_ignore.sort(key=strxfrm)
match = search(r'{0!s}{1!s}(\d+)\..*'.format(args.prefix,
args.separator), files_2_ignore[-1])
if match:
file_index = int(match.group(1))
if len(str(amount_files)) != len(str(file_index)):
print("Remove the 'ignore' flag and prozess all "
"files due to different index lengths!")
file_index = 0
amount_files_2_ignore = 0
amount_files_2_process = amount_files
files_2_process.extend(files_2_ignore)
else:
print(f"Error getting the index of the last file "
f"'{files_2_ignore[-1]}'!", file=stderr)
return 1
if not args.quiet:
if amount_files_2_process == 0:
print('ALL files are ignored!')
else:
print(f'{amount_files_2_process} files are moved and '
f'{amount_files_2_ignore} are ignored. Counter starts at '
f'{file_index + 1}.')
if args.random:
urandom = SystemRandom()
urandom.shuffle(files_2_process)
else:
files_2_process.sort(key=strxfrm)
file_index_length = len(str(file_index + amount_files_2_process))
if not (args.quiet or args.verbose) and amount_files_2_process > 10:
progressbar = True
bar_lenght = int(get_terminal_size()[0]) - 12
elif args.verbose:
print()
for old_file in files_2_process:
file_index += 1
file_counter += 1
old_file_path = '{0!s}/{1!s}'.format(args.path, old_file)
new_file = '{0!s}{1!s}{2:0{3}d}.{4!s}'.format(
args.prefix, args.separator, file_index, file_index_length,
args.extension
)
new_file_path = '{0!s}/{1!s}'.format(args.path, new_file)
if progressbar:
progress = file_counter / amount_files_2_process
print('\r{0:>4.0%} |{1:{2}s}|'.format(progress,
int(bar_lenght * progress) * '=', bar_lenght), end='')
if not args.ignore and old_file_path == new_file_path:
if args.verbose:
print(f"New file is equal old one: '{new_file}'")
continue
if isfile(new_file_path):
if args.verbose:
print(f"Moving file: '{new_file}' > '{new_file}.{BACKUP_EXT}'")
try:
move(new_file_path, new_file_path + '.' + BACKUP_EXT)
except (OSError, IOError) as error:
print(f"Error moving file: '{new_file}' > "
f"'{new_file}.{BACKUP_EXT}': {error}", file=stderr)
continue
else:
files_2_backup.append(new_file_path)
move_counter += 1
if old_file_path in files_2_backup:
old_file += '.' + BACKUP_EXT
old_file_path += '.' + BACKUP_EXT
if args.verbose:
print(f"Moving file: '{old_file}' > '{new_file}'")
try:
move(old_file_path, new_file_path)
except (OSError, IOError) as error:
print(f"Error moving file: '{old_file}' > '{new_file}': {error}",
file=stderr)
continue
else:
move_counter += 1
if progressbar:
print(' done')
if not args.quiet and amount_files_2_process > 0:
if args.verbose:
print()
max_moves = 2 * amount_files
print(f'Optimization: {move_counter} moves used instead of {max_moves}.'
f'\nSavings: {1 - move_counter / max_moves:0.0%}')
# ------------------------------------------------------------------------------
if __name__ == '__main__':
return_vaule = main()
exit(return_vaule)
~/dev/rename.py --extension='jpg' --path="${HOME}/pictures" --random