Created
July 24, 2022 15:23
-
-
Save novoid/1188417e5598263f4666bfaecf39d68f to your computer and use it in GitHub Desktop.
This tool renames a file according to its parent directory name
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
PROG_VERSION = "Time-stamp: <2022-03-13 22:56:40 vk>" | |
# TODO: | |
# - fix parts marked with «FIXXME» | |
# ===================================================================== ## | |
# You might not want to modify anything below this line if you do not ## | |
# know, what you are doing :-) ## | |
# ===================================================================== ## | |
from importlib import import_module | |
def save_import(library): | |
try: | |
globals()[library] = import_module(library) | |
except ImportError: | |
print("Could not find Python module \"" + library + | |
"\".\nPlease install it, e.g., with \"sudo pip install " + library + "\".") | |
sys.exit(2) | |
import re | |
import sys | |
import os | |
import argparse # for handling command line arguments | |
import logging | |
save_import('colorama') # for colorful output | |
# Determining the window size of the terminal: | |
try: | |
TTY_HEIGHT, TTY_WIDTH = [int(x) for x in os.popen('stty size', 'r').read().split()] | |
except ValueError: | |
TTY_HEIGHT, TTY_WIDTH = 80, 80 # fall-back values | |
PROG_VERSION_DATE = PROG_VERSION[13:23] | |
DESCRIPTION = "This tool renames a file according to its parent directory name.\n\ | |
\n\ | |
" | |
EPILOG = u"\n\ | |
:copyright: (c) by Karl Voit <[email protected]>\n\ | |
:license: GPL v3 or any later version\n\ | |
:URL: -\n\ | |
:bugreports: via github or <[email protected]>\n\ | |
:version: " + PROG_VERSION_DATE + "\n·\n" | |
parser = argparse.ArgumentParser(prog=sys.argv[0], | |
# keep line breaks in EPILOG and such | |
formatter_class=argparse.RawDescriptionHelpFormatter, | |
epilog=EPILOG, | |
description=DESCRIPTION) | |
parser.add_argument(dest="file", metavar='FILE', help='One file to rename') | |
parser.add_argument("-s", "--dryrun", dest="dryrun", action="store_true", | |
help="Enable dryrun mode: just simulate what would happen, do not modify file") | |
parser.add_argument("-v", "--verbose", | |
dest="verbose", action="store_true", | |
help="Enable verbose mode") | |
parser.add_argument("-q", "--quiet", | |
dest="quiet", action="store_true", | |
help="Enable quiet mode") | |
parser.add_argument("--version", | |
dest="version", action="store_true", | |
help="Display version and exit") | |
options = parser.parse_args() | |
def handle_logging(): | |
"""Log handling and configuration""" | |
if options.verbose: | |
FORMAT = "%(levelname)-8s %(asctime)-15s %(message)s" | |
logging.basicConfig(level=logging.DEBUG, format=FORMAT) | |
elif options.quiet: | |
FORMAT = "%(levelname)-8s %(message)s" | |
logging.basicConfig(level=logging.ERROR, format=FORMAT) | |
else: | |
FORMAT = "%(levelname)-8s %(message)s" | |
logging.basicConfig(level=logging.INFO, format=FORMAT) | |
def error_exit(errorcode, text): | |
"""exits with return value of errorcode and prints to stderr""" | |
sys.stdout.flush() | |
logging.error(text) | |
sys.exit(errorcode) | |
def split_up_filename(filename): | |
""" | |
Returns separate strings for the given filename. | |
If filename is not a Windows lnk file, the "basename | |
without the optional .lnk extension" is the same as | |
the basename. | |
@param filename: an unicode string containing a file name | |
@param return: filename with absolute path, pathname, basename, basename without the optional ".lnk" extension | |
""" | |
if not os.path.exists(filename): | |
# This does make sense for splitting up filenames that are about to be created for example: | |
logging.debug('split_up_filename(' + filename + | |
') does NOT exist. Playing along and returning non-existent filename parts.') | |
dirname = os.path.dirname(filename) | |
else: | |
dirname = os.path.dirname(os.path.abspath(filename)) | |
basename = os.path.basename(filename) | |
basename_without_lnk = basename | |
return os.path.join(dirname, basename), dirname, basename, basename_without_lnk | |
def print_item_transition(path, source, destination, filename): | |
""" | |
Returns true if item is member of at least one list in list_of_lists. | |
@param path: string containing the path to the files | |
@param source: string of basename of filename before transition | |
@param destination: string of basename of filename after transition or target | |
@param return: N/A | |
""" | |
transition_description = 'renaming' | |
style_destination = colorama.Style.BRIGHT + colorama.Back.GREEN + colorama.Fore.BLACK | |
destination = style_destination + os.path.basename(destination) + colorama.Style.RESET_ALL | |
max_file_length = len(filename) | |
if 15 + len(transition_description) + (2 * max_file_length) < TTY_WIDTH: | |
# probably enough space: screen output with one item per line | |
source_width = max_file_length | |
source = source | |
arrow_left = colorama.Style.DIM + '――' | |
arrow_right = '―→' | |
print(" {0:<{width}s} {1:s}{2:s}{3:s} {4:s}".format(source, | |
arrow_left, | |
transition_description, | |
arrow_right, | |
destination, | |
width=source_width)) | |
else: | |
# for narrow screens (and long file names): split up item source/destination in two lines | |
print(" {0:<{width}s} \"{1:s}\"".format(transition_description, | |
source, | |
width=len(transition_description))) | |
print(" {0:<{width}s} ⤷ \"{1:s}\"".format(' ', | |
destination, | |
width=len(transition_description))) | |
def handle_file(orig_filename, dryrun, quiet): | |
""" | |
@param orig_filename: string containing one file name with absolute path | |
@param dryrun: boolean which defines if files should be changed (False) or not (True) | |
@param return: error value or new filename | |
""" | |
assert(orig_filename.__class__ == str) | |
if dryrun: | |
assert(dryrun.__class__ == bool) | |
if quiet: | |
assert(quiet.__class__ == bool) | |
filename, dirname, basename, basename_without_lnk = split_up_filename(orig_filename) | |
parentdir = os.path.split(dirname)[1] | |
extension = os.path.splitext(basename)[1] | |
new_basename = parentdir.replace('.', ' ').replace(' ', ' ').replace(' ', ' ').strip() + extension | |
logging.debug("handle_file(\"" + filename + "\") " + '#' * 10 + | |
" … with working dir \"" + os.getcwd() + "\"") | |
if filename != new_basename: | |
if not quiet: | |
print_item_transition(dirname, basename, new_basename, new_basename) | |
if not dryrun: | |
os.rename(filename, new_basename) | |
logging.debug("handle_file(\"" + filename + "\") " + '#' * 10 + " finished") | |
return new_basename | |
def successful_exit(): | |
logging.debug("successfully finished.") | |
sys.stdout.flush() | |
sys.exit(0) | |
def main(): | |
"""Main function""" | |
if options.version: | |
print(os.path.basename(sys.argv[0]) + " version " + PROG_VERSION_DATE) | |
sys.exit(0) | |
handle_logging() | |
if options.verbose and options.quiet: | |
error_exit(1, "Options \"--verbose\" and \"--quiet\" found. " + | |
"This does not make any sense, you silly fool :-)") | |
filename = handle_file(options.file, options.dryrun, options.quiet) | |
successful_exit() | |
if __name__ == "__main__": | |
try: | |
main() | |
except KeyboardInterrupt: | |
logging.info("Received KeyboardInterrupt") | |
# END OF FILE ################################################################# | |
# end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment