Skip to content

X386 – Ubuntu & Python

Ubuntu and Python Documentation

  • Home
  • About
  • Contact
  • License
  • Privacy Policy

bulkrename.py Bulk Rename by Pattern

Posted on 24/05/2020 - 29/09/2020 by exforge
#!/usr/bin/env python3
"""
bulkrename.py is written to ease up renaming many files at once. You have to 
supply a directory (-d) where the files are, or as a default, current directory 
is used. 
You can verbose (-v) or quiet (-q) the output. By default . files are not renamed, 
you can add them with (-a). Renaming is done by following the pattern. All the 
occurences of the first character will be changed to the second, all of the third 
to fourth, fifth to sixth etc. If instead of changing, you want to delete a character, 
use / as the character to be changed.

    Copyright (C) 2020 Exforge exforge@x386.xyz
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""


import argparse
import os

def ren(ins, ddict):
    """ Renames in and returns the result by using the ddict dictionary.
    Every character existing as a key in dict is changed to that key's val.
    Changing must be done one by one to avoid multiple changes.
    """
    outs = ""
    for i in range(len(ins)):
        if ins[i] in ddict:
            outs = outs + ddict[ins[i]]
        else:
            outs = outs + ins[i]
    return outs

# A somewhat long program description, also can be considered as a comment

desc = "This program is written to ease up renaming many files at once. "  + \
    "You have to supply a directory (-d) where the files are, or as a default, " + \
    "current directory is used. You can verbose (-v) or quiet (-q) the output. " + \
    "By default . files are not renamed, you can add them with (-a). " + \
    "Renaming is done by following the pattern. All the occurences of the first " + \
    "character will be changed to the second, all of the third to fourth, fifth " + \
    "to sixth etc. If instead of changing, you want to delete a character, use / " + \
    "as the character to be changed." 

# Start parser
parser = argparse.ArgumentParser(description="Bulk Rename", epilog=desc)

# Add 2 mutually exclusive options
#  verbose or quiet, one or none can be selected
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", help = "verbose output",
        action="store_true")
group.add_argument("-q", "--quiet", help = "quiet mode, no output",
        action="store_true")

# Optional argument for directory of the files to be renames
parser.add_argument("-d", "--directory", default="./",
                    help="directory of the files to be renamed")

parser.add_argument("-a", "--all", 
                    help="do not ignore entries starting with .", action="store_true")



# Positional argument for the renaming pattern
"""Rename pattern is a string containing character to change and 
   character to be changed one by one. Thus RrAaCc means, R-->r, A-->a C-->c
   To remove a character instead of changing it, it must be followed by /
   Thus RrAaCcD/ means the same as above except all Ds will be removed.
   For repeated characters, only the last one will be used.
   """
parser.add_argument("pattern", 
        help = "Rename pattern. RrSs./  / is used to remove the char")

args = parser.parse_args()

# If given directory of files does not exist, leave the program with error code
if not os.path.isdir(args.directory):
    print("Directory", args.directory, "Does not exists. Exiting...")
    exit(1)


""" If there is an excess character, that is there are odd number of
    characters in the pattern, remove the last character
"""
ipattern = args.pattern
lenp = len(ipattern)
if lenp % 2 == 1:
    ipattern = ipattern[0:lenp-1]
lenp = len(ipattern)

""" Create a dict from the pattern, key --> char to be replaced
    val --> char to replace
"""
dpattern = dict()
for i in range(lenp//2):
    pat = ipattern[i*2+1]
    # / means char to be deleted, so null is assigned
    if pat == "/":
        pat = ""
    dpattern[ipattern[i*2]] = pat


# Get the directory of the files, 
#  put a / at the end if not already exists
path = args.directory
if path[-1] != "/":
    path = path + "/"

# Get the list of the items in the directory
files1 = os.listdir(path)        # List of everything in the directory
files1.sort()                    # It is easy to sort, so why not
files2 = list()                  # Only files will be added here
other_files = list()             # All others will be added here

# From all items (files, dirs) get only the files in a new list
#   All other items will be put the other_files list
for ffile in files1:
    if os.path.isfile(path + ffile):
        if ffile[0] == ".":
            if args.all:
                files2.append(ffile)
            else:
                other_files.append(ffile)
        else:
            files2.append(ffile)
    else:
        other_files.append(ffile)


# From the new list, remove the unchanging files and make a new list
#  move unchanging files to other_files list
files3 = list()

for ffile in files2:
    if ffile != ren(ffile, dpattern):
        files3.append(ffile)
    else:
        other_files.append(ffile)

# Now, all files to rename are in files3, all other files and dirs are in other_files

# Find the files that would cause a name conflict and put them in a skip list
#   if the new name of a file is already a name of other files, it will be skipped
skip_files = list()         # The list of skipped files because of the name conflict
files4 = list()             # Almost final list of files to be renamed

for ffile in files3:
    if ren(ffile, dpattern) in other_files:
        skip_files.append(ffile)
    else:
        files4.append(ffile)

""" Two (or more) of the files can be renamed to the same name in the list. 
    They will be found and put into the skip list.
    """
files5 = list()             # Final list of files to be renamed
skip2_files = list()        # Temporary place to keep renamed file name
for ffile in files4:
    # if the files renamed state would already be in the list
    #   put the file to the skip list
    if ren(ffile, dpattern) in skip2_files:
        skip_files.append(ffile)
    # Otherwise, file is OK, put its renamed state to the temporary list
    #   to check the other files with
    else:
        files5.append(ffile)
        skip2_files.append(ren(ffile, dpattern))


# If not quiet mode, display the skipped files.
if len(skip_files) != 0:
    if not args.quiet:
        print("The following files cause a name conflict.")
        print("So they will not be renamed:")
        for ffile in skip_files:
            print(path + ffile)

# If there are no files to rename, give a message and quit
if len(files5) == 0:
    if not args.quiet:
        print("No files to rename, exiting")
    exit(0)

""" In quiet mode do nothing. 
    In verbose mode, display the pattern, list all the files to rename and their renamed state. 
    In verbose and standart mode, ask user to continue.
    """

if args.verbose:
    print("Rename pattern:")
    for key in dpattern:
        print(key, "-->", dpattern[key])
    print()
    print("The following file renames will be processed:")
    for ffile in files5:
        print(path+ffile, "-->",  path + ren(ffile, dpattern))
    print()
if not args.quiet:
    print("!!!This process is irreversible!!!")         # I mean I cannot reverse it
    ans = input("Type Yes or Y to continue, any other to exit:")
    if ans not in ["Yes", "yes", "Y", "y"]:
        print("Exiting...")
        exit(2)
    print()

if not args.quiet:
    print("Renaming processing started...")

error_flag = False                  # Keep track of any rename error
error_files = list()                # Files with rename error

# Rename process
for ffile in files5:
    source = path + ffile
    dest = path + ren(ffile, dpattern)
    if args.verbose:
        print("Renaming", source, "to", dest)
    # try renaming
    try: 
        t = os.rename(source, dest)
    # error occured, put the file in error list
    except:
        error_flag = True
        error_files.append(ffile)
        if args.verbose:
            print("Rename is not successfull, possibly a name conflict")
    # rename successfull
    else:
        if args.verbose:
            print("Rename is successfull")

# Display finishing message, for verbose mode display renaming errors too   
message1 = "Rename process complete. "
message2 = "Some files couldn't be renamed."
message3 = "Following files also couldn't be renamed."
finish_message = message1
if error_flag:
    if args.verbose:
        finish_message = message1 + message3
    else:
        finish_message = message1 + message2

if not args.quiet:
    print(finish_message)

if args.verbose and error_flag:
    for ffile in error_files:
        print(path + ffile)

# This is the end

Posted in Python

Post navigation

Custom Complex Number Class
helptext2html.py Text to Html Converter
Proudly powered by WordPress | Theme: micro, developed by DevriX.