diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7d57ea0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,27 @@ +# License + +***Nomen*** - Multi-purpose rename tool + +Copyright © 2018 by Ralf Kilian + +Distributed under the *MIT License*: + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f34655 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# *Nomen* Nomen logo + +**Table of contents** +* [Definition](#definition) +* [Details](#details) +* [Requirements](#requirements) +* [Documentation](#documentation) +* [Useless facts](#useless-facts) + +---- + +## Definition + +The *Nomen* project is a multi-purpose rename tool to consistently rename the base name as well as the extension of files in a variety of ways and also to remove unnecessary whitespaces from directory names. + +[Top](#nomen-) + +## Details + +Basically, *Nomen* allows to consistently rename the base name as well as the extension of files and to remove unnecessary whitespaces from directory names. + +For now, it is capable of the following, briefly stated: + +* Convert the case of the base name (prefix or stem) of files. +* Convert the case of the extension (suffix) of files. +* Adjust differently spelled extensions from files of the same file type. +* Rename base names based on the name of the directory where the files are stored in. +* Modify base names by adding, removing or replacing certain strings. +* Remove leading, trailing and duplicate whitespaces from directory names. + +It also comes with an integrated simulation mode that simulates the rename process and writes the details into a report file. This allows checking which files would have been renamed. + +The project also consists of [multiple components](../../wiki#components). + +Please be sure to read [this](../../wiki#important-notice) before using *Nomen*. + +[Top](#nomen-) + +## Requirements + +In order to use *Nomen*, the *Python* framework must be installed on the system. + +Depending on which version of the framework you are using: + +* *Python* 2.x (version 2.7 or higher is recommended, may also work with earlier versions) +* *Python* 3.x (version 3.2 or higher is recommended, may also work with earlier versions) + +[Top](#nomen-) + +## Documentation + +In the corresponding `docs` sub-directories, there are plain text files containing a detailed documentation for each component with further information and usage examples. + +[Top](#nomen-) + +## Useless facts + +Whoever cares can find them [here](../../wiki#useless-facts). + +[Top](#nomen-) + diff --git a/nomen.png b/nomen.png new file mode 100644 index 0000000..bc89e91 Binary files /dev/null and b/nomen.png differ diff --git a/python2/cfg/sample_lower.cfg b/python2/cfg/sample_lower.cfg new file mode 100644 index 0000000..1e79765 --- /dev/null +++ b/python2/cfg/sample_lower.cfg @@ -0,0 +1,67 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be # +# LOWERCASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "foo" shall always be lowercase in file names. +# +# FOOBAR.TXT => file will NOT be renamed (because "FOO" is not a +# separate word) +# FOO BAR.TXT => file will be renamed to "foo BAR.txt" +# +foo + +# Example 2: +# +# The string "foo" shall always be lowercase in file names. +# +# FOOBAR.TXT => file will be renamed to "fooBAR.txt" +# FOO BAR.TXT => file will be renamed to "foo BAR.txt" +# +$(foo) + +# Example 3: +# +# The string "bar" shall always be lowercase in file names. +# +# FOOBAR.TXT => file will be renamed to "FOObar.txt" +# FOO BAR.TXT => file will be renamed to "FOO bar.txt" +# +$(bar) + +# Example 4: +# +# When converting the case of the files +# +# FOOBAR'S ADVENTURE.TXT +# MARTIN O'SULLIVAN.TXT +# +# using title case, the file will be renamed as follows: +# +# Foobar'S Adventure.TXT +# Martin O'Sullivan.TXT +# +# So, if you want to convert the "'S" to lowercase, you could use +# +# $('s) +# +# but this will also rename "O'Sullivan" to "O'sullivan". To avoid this you +# may use the following statement. It only converts "'S" to lowercase if a +# whitespace follows. +# +$('s ) + +# EOF + diff --git a/python2/cfg/sample_mixed.cfg b/python2/cfg/sample_mixed.cfg new file mode 100644 index 0000000..7a42e96 --- /dev/null +++ b/python2/cfg/sample_mixed.cfg @@ -0,0 +1,67 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be MIXED # +# CASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "FoO" shall always be mixed case in file names. +# +# FOOBAR.TXT => file will NOT be renamed (because "FOO" is not a +# separate word) +# FOO BAR.TXT => file will be renamed to "FoO BAR.txt" +# +FoO + +# Example 2: +# +# The string "FoO" shall always be mixed case in file names. +# +# FOOBAR.TXT => file will be renamed to "FoOBAR.txt" +# FOO BAR.TXT => file will be renamed to "FoO BAR.txt" +# +$(FoO) + +# Example 3: +# +# The string "bAr" shall always be mixed case in file names. +# +# FOOBAR.TXT => file will be renamed to "FOObAr.txt" +# FOO BAR.TXT => file will be renamed to "FOO bAr.txt" +# +$(bAr) + +# Example 4: +# +# When converting the case of the files +# +# FOOBAR'S ADVENTURE.TXT +# MARTIN O'SULLIVAN.TXT +# +# using title case, the file will be renamed as follows: +# +# Foobar'S Adventure.TXT +# Martin O'Sullivan.TXT +# +# So, if you want to convert the "'S" to lowercase, you could use +# +# $('s) +# +# but this will also rename "O'Sullivan" to "O'sullivan". To avoid this you +# may use the following statement. It only converts "'S" to lowercase if a +# whitespace follows. +# +$('s ) + +# EOF + diff --git a/python2/cfg/sample_title.cfg b/python2/cfg/sample_title.cfg new file mode 100644 index 0000000..2eecc26 --- /dev/null +++ b/python2/cfg/sample_title.cfg @@ -0,0 +1,45 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be TITLE # +# CASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "Foo" shall always be title case in file names. +# +# FOOBAR.TXT => file will NOT be renamed (because "FOO" is not a +# separate word) +# FOO BAR.TXT => file will be renamed to "Foo BAR.txt" +# +Foo + +# Example 2: +# +# The string "Foo" shall always be title case in file names. +# +# FOOBAR.TXT => file will be renamed to "FooBAR.txt" +# FOO BAR.TXT => file will be renamed to "Foo BAR.txt" +# +$(Foo) + +# Example 3: +# +# The string "Foo" shall always be title case in file names. +# +# FOOBAR.TXT => file will be renamed to "FOOBar.txt" +# FOO BAR.TXT => file will be renamed to "FOO Bar.txt" +# +$(Bar) + +# EOF + diff --git a/python2/cfg/sample_upper.cfg b/python2/cfg/sample_upper.cfg new file mode 100644 index 0000000..6eb268a --- /dev/null +++ b/python2/cfg/sample_upper.cfg @@ -0,0 +1,45 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be # +# UPPERCASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "FOO" shall always be uppercase in file names. +# +# foobar.txt => file will NOT be renamed (because "foo" is not a +# separate word) +# foo bar.txt => file will be renamed to "FOO bar.txt" +# +FOO + +# Example 2: +# +# The string "FOO" shall always be uppercase in file names. +# +# foobar.txt => file will be renamed to "FOObar.txt" +# foo bar.txt => file will be renamed to "FOO bar.txt" +# +$(FOO) + +# Example 3: +# +# The string "BAR" shall always be uppercase in file names. +# +# foobar.txt => file will be renamed to "fooBAR.txt" +# foo bar.txt => file will be renamed to "foo BAR.txt" +# +$(BAR) + +# EOF + diff --git a/python2/changelog.txt b/python2/changelog.txt new file mode 100644 index 0000000..b2a03db --- /dev/null +++ b/python2/changelog.txt @@ -0,0 +1,195 @@ + +CHANGELOG (Nomen) + + Version 2.3.5 (2018-04-14) + + + Added new versions of the Clap and PaVal core modules (replaced the + existing ones). + + * Revised (refurbished) all components of the project in general + (negligible changes). + + Version 2.3.4 (2016-07-12) + + + Added a simple random string generator method to the Nomen Common + core module. + + * Revised the confirmation prompt inside the Nomen Common core module + (now uses random strings instead of a fixed one). + + # Fixed the type mismatch bug inside the Nomen File Renamer core + module (occurred in case a file had the correct prefix but without + a trailing number). + + Version 2.3.3 (2016-06-20) + + + Added a new feature to the Nomen File Renamer component that allows + to order the files by their access, creation and modification time. + + # Fixed the maximum recursion depth exceeded bug inside the Nomen + Common core module (occurred under certain circumstances, only) + + Version 2.3.2 (2016-05-19) + + * Revised some code inside the Nomen Directory Name Space Modifier + script changes). + + # Fixed the path referenced before assignment bug inside the Nomen + Common core module (occurred under certain circumstances, only). + + Version 2.3.1 (2016-05-17) + + + Added a basic exclude feature to the Nomen Directory Name Space + Modifier script (case-insensitive, without regex syntax). + + # Fixed the character replacement bug inside a method of the Nomen + Common core module (used by Nomen Directory Name Space Modifier) + which occurred when the recurisve parameter was set. + + Version 2.3.0 (2016-04-16) + + + Added a new feature to the Nomen File Renamer component that allows + user-defined steps between each numeric ID. + + * Revised some code inside the Nomen Common core module (negligible + changes). + * Revised some parameter validation code in various core modules. + + Version 2.2.6 (2015-06-05) + + + Added some additional command-line arguments to the Nomen Directory + Name Space Modifier script to insert and remove spaces around + brackets and hyphens. + + * Revised some code inside the Nomen Common core module (negligible + changes). + + # Fixed the character replacement bug inside a method of the Nomen + Common core module (used by Nomen Directory Name Space Modifier). + + Version 2.2.5 (2015-01-24) + + + Added an optional feature to the Nomen Directory Name Space Modifier + (former "Nomen Directory Name Space Remover") to insert and remove + spaces next to punctuation characters. + + * Revised the confirmation prompt inside the Nomen Common core module. + + Version 2.2.4 (2015-01-03) + + * Revised the code for handling regular expressions inside the core + modules. + + - Removed unnecessary imports and unused variables from the core + modules. + + Version 2.2.3 (2014-08-25) + + + Added the Nomen Directory Name Space Remover component. + + * Revised (enhanced) the Nomen Common core module. + + Version 2.2.2 (2014-06-28) + + + Added a feature that allows omitting the config file path if the + file is located in the corresponding config sub-directory. + + * Revised the method to parse the case config files inside the Nomen + Static Case core module (added default values). + + Version 2.2.1 (2014-05-22) + + + Added an optional command-line argument to the Nomen File Name + Modifier script to remove certain leading and trailing characters + from the processed base name. + + # Fixed the file skip bug inside the Nomen File Name Modifier module + when processing files whose base name contains dots. + + Version 2.2.0 (2014-05-16) + + + Added the new Nomen File Name Modifier component. + + * Revised (reduced) some code inside the Nomen Static Case core + module. + * Revised (renamed) some command-line arguments inside all scripts + (for more clarity). + + - Removed some unnecessary code from the Nomen File Renamer core + module. + + Version 2.1.6 (2014-05-14) + + + Added a method to the Nomen Common core module that returns invalid + file name characters. + + * Revised (reduced) some code inside the Nomen File Renamer as the + well as Nomen Extension Renamer core module (negligible changes). + * Revised the list of invalid file name characters. + + Version 2.1.5 (2014-04-22) + + * Revised (reduced) the code inside the Nomen Common core module that + determines if a file with the new name already exists. + * Revised (reduced) the code inside the Nomen Common core module that + determines the case sensitivity of the file system. + + # Fixed the rename bug ("no such file or directory") inside the Nomen + Common core module when renaming files using the "rename-new" or + "fill-gaps" mode (only occurred on case-insensitive file systems). + + Version 2.1.4 (2014-04-19) + + # Fixed the attribute error inside the code of the Nomen Common core + module that determines if a file with the new name already exists. + # Fixed the file system case sensitivity bug inside Nomen Common core + module (which did not determine the case sensitivity properly). + # Fixed the list bug inside the Nomen File Renamer core module (which + did not append all skipped files to the list when using "fill-gaps" + mode). + + Version 2.1.3 (2014-04-12) + + + Added an optional command-line argument to the Nomen File Renamer + script to set a custom file name. + + * Revised the message text of some exceptions (for more clarity). + + Version 2.1.2 (2014-03-28) + + * Revised (reduced) the code inside the Nomen Common core module to + check if a file already has been processed. + * Revised the code inside the Nomen Common core module to check if the + file system is case-sensitive. + + Version 2.1.1 (2014-03-22) + + + Added an optional command-line argument to the Nomen File Renamer + script to ignore the file extension when numerating files. + + Added the estimated time to the simulation report file. + + * Revised (reduced) some code inside the Nomen Common core module. + + # Fixed the overwrite bug that occurred due to different cases of the + file extensions when renaming files via the Nomen File Renamer on + case-insensitive file systems (such as FAT32 and NTFS). + + Version 2.1.0 (2014-03-19) + + * Revised the "fill" rename methods (merged them to one method and + removed unused variables) inside the Nomen File Renamer core module. + * Revised (changed) the names of the rename modes inside the Nomen + File Renamer component (for more clarity). + * Revised the way of renaming files (numeration is now dependent of + the file extension) inside the Nomen File Renamer core module. + + # Fixed the attribute error inside the Nomen Common core module when + reading out the major version of the Python framework using Python + version 2.6 or below. + # Fixed the empty list bug inside the Nomen File Renamer core module + (which terminated the whole file rename process, if occurred). + + Version 2.0.0 (2014-03-18) + + * First official release of this major version. + diff --git a/python2/core/__init__.py b/python2/core/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/python2/core/clap.py b/python2/core/clap.py new file mode 100755 index 0000000..03305c0 --- /dev/null +++ b/python2/core/clap.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Clap - Command-line argument parser module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/clap +# ============================================================================ + +__version__ = "1.1.10" + + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + + +class Parser(object): + """ + Project independent command-line argument parser class. + """ + __arg_grp_opt = None + __arg_grp_req = None + __arg_parser = None + __is_argparser = False + __conflict_handler = "resolve" # used by OptionParser, only + + def __init__(self): + try: + from argparse import ArgumentParser + self.__arg_parser = ArgumentParser(add_help=False) + self.__arg_grp_req = \ + self.__arg_parser.add_argument_group("required arguments") + self.__arg_grp_opt = \ + self.__arg_parser.add_argument_group("optional arguments") + self.__is_argparser = True + return + except ImportError: + # Ignore the exception and proceed with the fallback + pass + + try: + from optparse import OptionParser + self.__arg_parser = \ + OptionParser(conflict_handler=self.__conflict_handler) + self.__arg_grp_req = \ + self.__arg_parser.add_option_group("Required arguments") + self.__arg_grp_opt = \ + self.__arg_parser.add_option_group("Optional arguments") + return + except ImportError: + # This should never happen + raise ImportError("Failed to initialize an argument parser.") + + def add_avalue(self, arg_short, arg_long, arg_help, arg_dest, arg_default, + arg_required): + """ + Add an argument that expects a single user-defined value. + """ + if arg_required: + obj = self.__arg_grp_req + else: + obj = self.__arg_grp_opt + + if arg_default is not None: + # Enclose the value with quotes in case it is not an integer + quotes = "'" + try: + arg_default = int(arg_default) + quotes = "" + except ValueError: + pass + + if arg_help.strip().endswith(")"): + arg_help = arg_help.rstrip(")") + arg_help += ", default is %s%s%s)" % \ + (quotes, str(arg_default), quotes) + else: + arg_help += " (default is %s%s%s)" % \ + (quotes, str(arg_default), quotes) + + if self.__is_argparser: + if arg_short is None: + obj.add_argument(arg_long, help=arg_help, dest=arg_dest, + default=arg_default, required=arg_required) + else: + obj.add_argument(arg_short, arg_long, help=arg_help, + dest=arg_dest, default=arg_default, + required=arg_required) + else: + if arg_short is None: + obj.add_option(arg_long, help=arg_help, dest=arg_dest, + default=arg_default) + else: + obj.add_option(arg_short, arg_long, help=arg_help, + dest=arg_dest, default=arg_default) + + def add_predef(self, arg_short, arg_long, arg_help, arg_dest, arg_choices, + arg_required): + """ + Add an argument that expects a certain predefined value. + """ + if arg_required: + obj = self.__arg_grp_req + else: + obj = self.__arg_grp_opt + + if self.__is_argparser: + if arg_short is None: + obj.add_argument(arg_long, help=arg_help, dest=arg_dest, + choices=arg_choices, required=arg_required) + else: + obj.add_argument(arg_short, arg_long, help=arg_help, + dest=arg_dest, choices=arg_choices, + required=arg_required) + else: + if arg_short is None: + obj.add_option(arg_long, help=arg_help, dest=arg_dest, + choices=arg_choices) + else: + # The OptionParser does not print the values to choose from, + # so these have to be added manually to the description of + # the argument first + arg_help += " (choose from " + for item in arg_choices: + arg_help += "'%s', " % item + arg_help = arg_help.rstrip(", ") + ")" + + obj.add_option(arg_short, arg_long, help=arg_help, + dest=arg_dest) + + def add_switch(self, arg_short, arg_long, arg_help, arg_dest, arg_store, + arg_required): + """ + Add an argument that does not expect anything, but returns a + boolean value. + """ + if arg_required: + obj = self.__arg_grp_req + else: + obj = self.__arg_grp_opt + + if arg_store: + arg_store = "store_true" + else: + arg_store = "store_false" + + if self.__is_argparser: + if arg_short is None: + obj.add_argument(arg_long, help=arg_help, dest=arg_dest, + action=arg_store, required=arg_required) + else: + obj.add_argument(arg_short, arg_long, help=arg_help, + dest=arg_dest, action=arg_store, + required=arg_required) + else: + if arg_short is None: + obj.add_option(arg_long, help=arg_help, dest=arg_dest, + action=arg_store) + else: + obj.add_option(arg_short, arg_long, help=arg_help, + dest=arg_dest, action=arg_store) + + def dependency(self, arg_name, arg_value, dependency): + """ + Check the dependency of a command-line argument. + """ + if dependency is not None: + if arg_value is None or str(arg_value) == "": + raise Exception("The '%s' argument depends on %s'." % + (arg_name, dependency)) + + def error(self, obj): + """ + Raise an error and cause the argument parser to print the error + message. + """ + if type(obj) == str: + obj = obj.strip() + + self.__arg_parser.error(obj) + + def parse_args(self): + """ + Parse and return the command-line arguments. + """ + if self.__is_argparser: + args = self.__arg_parser.parse_args() + else: + (args, values) = self.__arg_parser.parse_args() + return args + + def print_help(self): + """ + Print the usage, description, argument details and epilog. + """ + self.__arg_parser.print_help() + + def set_description(self, string): + """ + Set the description text. + """ + self.__arg_parser.description = string.strip() + + def set_epilog(self, string): + """ + Set the epilog text. + """ + self.__arg_parser.epilog = string.strip() + +# EOF diff --git a/python2/core/common.py b/python2/core/common.py new file mode 100755 index 0000000..243e332 --- /dev/null +++ b/python2/core/common.py @@ -0,0 +1,555 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Common core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +import os +import paval as pv +import random +import re +import sys +import tempfile + +from datetime import datetime as dt + +def compile_regex(string, ignore_case=True, regex_syntax=False): + """ + Compile a regular expression from the given pattern string. + """ + pv.string(string, "regular expression", True, None) + if regex_syntax: + pattern = ".*" + string + ".*" + else: + spec_chars = [ "\\", ".", "^", "$", "+", "?", "{", "}", "[", "]", + "|", "(", ")" ] + for char in spec_chars: + string = string.replace(char, "\\" + char) + string = string.strip("*").strip(";") + while ("*" * 2) in string: + string = string.replace(("*" * 2), "*") + while (";" * 2) in string: + string = string.replace((";" * 2), ";") + + list_string = string.split(";") + if len(list_string) > 0: + pattern = "" + for crit in list_string: + if not crit == "": + pattern += "(.*" + crit.replace("*", ".*") + ".*)|" + pattern = pattern.rstrip("|") + if pattern == "": + raise Exception("The given string does not make sense this " \ + "way.") + + if ignore_case: + regex = re.compile(pattern, re.IGNORECASE) + else: + regex = re.compile(pattern) + + return regex + +def confirm_notice(): + """ + Display a notice which must be confirmed by the user to proceed. + """ + string = random_string(6, True, True, True, True) + proceed = False + notice_text = """ o o o 88 + 8 8 88 + 8 8 .oPYo. oPYo. odYo. o8 odYo. .oPYo. 88 + 8 db 8 .oooo8 8 '' 8' '8 8 8' '8 8 8 88 + 'b.PY.d' 8 8 8 8 8 8 8 8 8 8 + '8 8' 'YooP8 8 8 8 8 8 8 'YooP8 88 + . 8 + 'oooP' + +Please use this tool with care to avoid data damage or loss! + +There is no function to undo the changes done by this tool, so you +should be aware of what you are doing. Improper use (e. g. modifying +files inside system directories) will corrupt your system! + +If you wish to proceed, type '%s' (case-sensitive, without any +quotes or spaces) and press the key. Otherwise, the process +will be cancelled.""" % string + + print_text_box("", notice_text) + choice = raw_input("> ") + + if choice == string: + choice = "Proceeding." + proceed = True + else: + choice = "cancelled." + print "\n%s\n" % choice + + return proceed + +def dir_space_modifier(directory, remove_duplicate=False, + remove_leading=False, remove_trailing=False, + brackets=False, hyphens=False, punctuation=False, + ignore_symlinks=False, recursive=False, exclude=None): + """ + Modify a directory name by removing leading, trailing and duplicate + spaces or by inserting and removing spaces next to punctuation + characters. + """ + + list_exclude = [] + if not exclude == None: + list_exclude = exclude.split(";") + + for item in os.listdir(directory): + excluded = False + if os.path.isdir(os.path.join(directory, item)): + path = os.path.join(directory, item) + for excl in list_exclude: + if excl.lower() in path.lower(): + excluded = True + break + else: + continue + + if excluded: + nextdir = path + else: + if remove_duplicate: + while (" " * 2) in item: + item = item.replace((" " * 2), " ") + + if hyphens: + item = item.replace("-", " - ") + while "- " in item: + item = item.replace("- ", "- ") + while " -" in item: + item = item.replace(" -", " -") + + if brackets: + while "( " in item: + item = item.replace("( ", "(") + item = item.replace("(", " (") + while " )" in item: + item = item.replace(" )", ")") + item = item.replace(")", ") ").replace(") .", ").") + while "[ " in item: + item = item.replace("[ ", "[") + item = item.replace("[", " [") + while " ]" in item: + item = item.replace(" ]", "]") + item = item.replace("]", "] ").replace("] .", "].") + item = item.replace("( [", "([").replace("] )", "])") + item = item.replace("[ (", "[(").replace(") ]", ")]") + + if punctuation: + item = item.replace(".", ". ") + while " ." in item: + item = item.replace(" .", ".") + item = item.replace(",", ", ") + while " ," in item: + item = item.replace(" ,", ",") + item = item.replace(":", ": ") + while " :" in item: + item = item.replace(" :", ":") + item = item.replace(";", "; ") + while " ;" in item: + item = item.replace(" ;", ";") + item = item.replace("!", "! ") + while " !" in item: + item = item.replace(" !", "!") + item = item.replace("?", "? ") + while " ?" in item: + item = item.replace(" ?", "?") + remove_leading = True + remove_trailing = True + + if remove_leading: + item = item.lstrip() + if remove_trailing: + item = item.rstrip() + + newpath = os.path.join(directory, item) + if remove_duplicate: + # Repeat this step after the replace actions above + while (" " * 2) in newpath: + newpath = newpath.replace((" " * 2), " ") + + if not os.path.exists(newpath): + os.rename(path, newpath) + nextdir = newpath + else: + nextdir = path + + if recursive: + dir_space_modifier(nextdir, remove_duplicate, remove_leading, + remove_trailing, brackets, hyphens, + punctuation, ignore_symlinks, True, exclude) + +def file_exists(file_path, list_files, fs_case_sensitive): + """ + Check if a file already exists on the file system as well as in a + given list. + """ + file_path = os.path.abspath(file_path) + if os.path.exists(file_path): + file_exists = True + else: + file_exists = False + + for item in list_files: + if item[1] == None: + item[1] = "" + + if fs_case_sensitive: + if file_path == item[1] or file_path == item[2]: + file_exists = True + break + else: + if file_path.lower() == item[1].lower() or \ + file_path.lower() == item[2].lower(): + file_exists = True + break + + return file_exists + +def format_timestamp(float_stamp=0): + """ + Convert a timestamp float into a readable format. + """ + return str(dt.fromtimestamp(float(str(float_stamp)))) + +def get_files(directory, recursive=False, ignore_case=True, regex=None, + regex_exclude=True, ignore_symlinks=False, order_by=None): + """ + Get the files and sub-directories from the given directory. + """ + pv.path(directory, "given", False, True) + + directory = os.path.abspath(directory) + list_files = [] + list_excluded = [] + + list_files, list_excluded = \ + __get_files( \ + directory, ignore_case, regex, regex_exclude, ignore_symlinks, + recursive, list_files, list_excluded, order_by) + + if order_by == None: + list_files.sort() + list_excluded.sort() + + return list_files, list_excluded + +def get_fs_case_sensitivity(directory): + """ + Determine if the file system of the given directory is case-sensitive. + """ + # This should be done with every directory that is processed, due to the + # fact, that e. g. a device containing a case-insensitive file system can + # be mounted into a directory of a case-sensitive file system. + + pv.path(directory, "given", False, True) + directory = os.path.abspath(directory) + + fd_temp, file_temp = tempfile.mkstemp(dir=directory) + file_name = os.path.basename(file_temp) + if os.path.exists(os.path.join(directory, file_name.upper())): + fs_case_sensitive = False + else: + fs_case_sensitive = True + os.close(fd_temp) + os.remove(file_temp) + + return fs_case_sensitive + +def get_invalid_chars(): + """ + Return the invalid file name characters (which must or should not be + part of a file name). + """ + # This list of characters depends on the file system where the files are + # being renamed on. Due to the fact, that e. g. a device containing a + # different file system can be mounted into a directory of the local file + # system, the following characters will be handled as invalid on every + # file system. + invalid_chars = "/\\?%*:|\"<>\n\r\t" + + return invalid_chars + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def print_text_box(heading, text): + """ + Print a text message outlined with an ASCII character frame. + """ + heading = heading.strip() + if len(heading) > 72: + raise Exception("The text box heading must not be longer than 72 " \ + "characters.") + if text == "": + raise Exception("The text box text must not be empty.") + + text_box = "\n+" + ("-" * 76) + "+" + \ + "\n|" + (" " * 76) + "|" + if not heading == "": + padding = int((72 - len(heading)) / 2) + heading = (" " * (padding + 2) + heading).ljust(76, " ") + text_box += ("\n|%s|\n|" + (" " * 76) + "|") % heading + list_text = text.split("\n") + for text in list_text: + list_words = text.split(" ") + count = 1 + line = "" + for word in list_words: + if len(line + word + " ") > 73: + text_box += "\n| " + line.ljust(74, " ") + "|" + line = word + " " + else: + line = line + word + " " + count += 1 + if count > len(list_words): + text_box += "\n| " + line.ljust(74, " ") + "|" + text_box += "\n|" + (" " * 76) + "|" \ + "\n+" + ("-" * 76) + "+\n" + + print text_box + +def random_string(length, uppercase=True, lowercase=False, numbers=False, + unique=False): + """ + Generate a random string out of literals and numbers. + """ + literals = "ABCDEFGHIJLMNOPQRSTUVWXYZ" + numbers = "0123456789" + chars = "" + string = "" + + if uppercase: + chars += literals + if lowercase: + chars += literals.lower() + if numbers: + chars += numbers + + if len(chars) == 0: + return string + if len(chars) < length: + length = len(chars) + + while len(string) < length: + rnd = random.randint(0, len(chars) - 1) + char = chars[rnd] + if unique: + if char in string: + continue + string += char + + return string + + +def rename(list_files, reverse=False): + """ + Rename the files which have neither been excluded nor skipped. + """ + list_skipped = [] + + if len(list_files) > 0: + if reverse: + list_files = reversed(list_files) + + for item in list_files: + if os.path.exists(item[0]): + if os.path.exists(item[2]): + list_skipped.append([item[0], item[1], item[2]]) + continue + + # In some cases the file will get a temporary name first and + # then its name will be changed to what it should be. + # + # This behaviour is required when using file systems that are + # case-insensitive (such as FAT32 or NTFS) where e. g. the + # file "FOOBAR.txt" would overwrite the file "foobar.txt" + # inside the same directory. + if item[1] == None or \ + item[1] == "": + os.rename(item[0], item[2]) + else: + os.rename(item[0], item[1]) + os.rename(item[1], item[2]) + + if len(list_skipped) > 0: + if not list_skipped == list_files: + rename(list_skipped, reverse) + +def report(report_file=None, list_header=[], list_renamed=[], + list_excluded=[], list_skipped=[], time_start=None): + """ + Write the details of the simulated rename process (simulation report) + into a file. + """ + files_total = str(len(list_renamed) + len(list_excluded) + \ + len(list_skipped)) + just = len(files_total) + files_renamed = str(len(list_renamed)).rjust(just, " ") + files_excluded = str(len(list_excluded)).rjust(just, " ") + files_skipped = str(len(list_skipped)).rjust(just, " ") + time_end = dt.now() + + try: + time_elapsed = str(time_end - time_start) + time_start = str(time_start) + except: + raise Exception("An invalid start date was given.") + + output = "\r\n" + "=" * 78 + \ + "\r\nFile type: " + list_header[0] + \ + "\r\n" + "-" * 78 + + for i in range(1, len(list_header)): + output += "\r\n" + list_header[i][0].ljust(20, " ") + \ + str(list_header[i][1]) + + output += "\r\n" + "-" * 78 + \ + "\r\nFiles renamed: " + files_renamed + \ + "\r\nFiles excluded: " + files_excluded + \ + "\r\nFiles skipped: " + files_skipped + \ + "\r\nFiles total: " + files_total + \ + "\r\n" + "-" * 78 + \ + "\r\nTimestamp: " + time_start[:-7] + \ + "\r\nElapsed time: " + time_elapsed + \ + "\r\nNomen version: " + __version__ + \ + "\r\n" + "=" * 78 + "\r\n\r\n" + + if len(list_renamed) > 0: + output += "\r\n [Renamed]\r\n" + for item in list_renamed: + output += " - Old: %s\r\n" % item[0] + output += " - New: %s\r\n\r\n" % item[2] + output += "\r\n" + + if len(list_excluded) > 0: + output += "\r\n [Excluded]\r\n" + for item in list_excluded: + output += " - %s\r\n" % item + output += "\r\n" + + if len(list_skipped) > 0: + output += "\r\n [Skipped]\r\n" + for item in list_skipped: + output += " - %s\r\n" % item + output += "\r\n" + + fh_report = open(report_file, "wb") + + # Run the appropriate code for the Python framework used + if sys.version_info[0] == 2: + fh_report.write(output) + elif sys.version_info[0] > 2: + fh_report.write(output.encode(sys.getdefaultencoding())) + + fh_report.close() + +def __get_files(directory, ignore_case, regex, regex_exclude, ignore_symlinks, + recursive, list_content, list_excluded, order_by): + """ + Core method to get the files from the given directory and its + sub-directories. + """ + list_dirs = [] + list_files = [] + + for item in os.listdir(directory): + path = os.path.join(directory, item) + if ignore_symlinks: + if os.path.islink(path): + continue + if os.path.isfile(path): + if regex == None: + list_files.append(path) + else: + if regex_exclude: + if ignore_case: + if regex.match(item.lower()): + list_excluded.append(path) + else: + list_files.append(path) + else: + if regex.match(item): + list_excluded.append(path) + else: + list_files.append(path) + else: + if ignore_case: + if regex.match(item.lower()): + list_files.append(path) + else: + list_excluded.append(path) + else: + if regex.match(item): + list_files.append(path) + else: + list_excluded.append(path) + else: + list_dirs.append(path) + + if len(list_files) > 0: + if order_by == None: + list_files.sort() + else: + list_files = __set_order(list_files, order_by) + list_content.append([directory, list_files]) + + if recursive: + for directory in list_dirs: + list_content, list_excluded = \ + __get_files(directory, ignore_case, regex, regex_exclude, + ignore_symlinks, True, list_content, + list_excluded, order_by) + + return list_content, list_excluded + +def __set_order(file_list, order_by): + """ + Set a certain order of the files before renaming them. + """ + list_files = [] + list_temp = [] + + for file_name in file_list: + if "." in file_name: + file_ext = file_name.split(".")[-1] + else: + file_ext = "" + + time_access = format_timestamp(os.stat(file_name).st_atime) + time_create = format_timestamp(os.stat(file_name).st_ctime) + time_modify = format_timestamp(os.stat(file_name).st_mtime) + + if order_by == "accessed": + list_temp.append([time_access, file_name, file_ext]) + elif order_by == "created": + list_temp.append([time_create, file_name, file_ext]) + else: + list_temp.append([time_modify, file_name, file_ext]) + + list_temp.sort() + for item in list_temp: + list_files.append(item[1]) + + return list_files + +# EOF + diff --git a/python2/core/extren.py b/python2/core/extren.py new file mode 100755 index 0000000..790c333 --- /dev/null +++ b/python2/core/extren.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Extension renamer core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +import common +import os +import paval as pv +import re + +from datetime import datetime as dt + +def convert_case(directory, case, conflict_mode, recursive=False, + report_file=None, ignore_symlinks=False): + """ + Convert the case of the file extensions. + """ + pv.path(directory, "given", False, True) + pv.compstr(case, "case", ["lower", "title", "upper"]) + pv.compstr(conflict_mode, "conflict mode", ["rename", "skip"]) + + case = case.lower() + conflict_mode = conflict_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = re.compile("(.*)") + + list_content, list_excluded = \ + common.get_files(directory, recursive, True, regex, False, + ignore_symlinks) + for item in list_content: + list_files = item[1] + list_renamed, list_skipped = \ + __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive) + + if simulate: + list_header = [] + list_header.append("Nomen Extension Case Converter simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Conflict mode:", conflict_mode.capitalize()]) + list_header.append(["Case:", case.capitalize()]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def rename_extensions(directory, conflict_mode, extension, extension_target, + recursive=False, ignore_case=True, report_file=None, + ignore_symlinks=False): + """ + Rename the file extensions in the given directory and all of its + sub-directories (if requested). + """ + pv.path(directory, "given", False, True) + pv.compstr(conflict_mode, "conflict mode", ["rename", "skip"]) + pv.string(extension, "extension", False, common.get_invalid_chars()) + pv.string(extension_target, "target extension", False, + common.get_invalid_chars()) + + conflict_mode = conflict_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_extensions = [] + list_renamed = [] + list_skipped = [] + + if ";" in extension: + while (";" * 2) in extension: + extension = extension.replace((";" * 2), ";") + + list_temp = extension.split(";") + for extension in list_temp: + if not extension == "": + list_extensions.append(extension) + + if len(list_extensions) == 0: + raise Exception("The given extension list does not contain any " \ + "extensions.") + else: + list_extensions.append(extension) + + pattern = "" + for extension in list_extensions: + pattern += "(.*\." + str(extension) + "$)|" + pattern = pattern.rstrip("|") + + if ignore_case: + regex = re.compile(pattern, re.IGNORECASE) + else: + regex = re.compile(pattern) + + list_content, list_excluded = \ + common.get_files(directory, recursive, ignore_case, regex, False, + ignore_symlinks) + for item in list_content: + list_files = item[1] + list_renamed, list_skipped = \ + __rename_extensions(list_files, list_extensions, list_renamed, + list_skipped, conflict_mode, extension_target) + + if simulate: + list_header = [] + list_header.append("Nomen Extension Renamer simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Conflict mode:", conflict_mode.capitalize()]) + list_header.append(["Extensions:", extension]) + list_header.append(["Target extension:", extension_target]) + list_header.append(["Ignore case:", ignore_case]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive): + """ + Core method to convert the case of the file extensions. + """ + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + for file_path in list_files: + num = 1 + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + file_ext = os.path.splitext(file_name)[1] + + if file_ext == "": + list_skipped.append(file_path) + continue + + if case == "lower": + extension_target = file_ext.lower() + elif case == "title": + extension_target = file_ext.title() + elif case == "upper": + extension_target = file_ext.upper() + + file_newpath = file_path.replace(file_ext, extension_target) + if (file_path == file_newpath): + list_skipped.append(file_path) + continue + + if conflict_mode == "rename": + while True: + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if file_path.lower() == file_newpath.lower(): + break + + file_newpath = \ + file_path.replace(file_ext, + "_" + str(num) + extension_target) + num += 1 + else: + break + elif conflict_mode == "skip": + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if not file_path.lower() == file_newpath.lower(): + list_skipped.append(file_newpath) + continue + else: + list_skipped.append(file_newpath) + continue + + if os.path.exists(file_path): + list_renamed.append([file_path, file_newpath + ".__temp__", + file_newpath]) + + return list_renamed, list_skipped + +def __rename_extensions(list_files, list_extensions, list_renamed, + list_skipped, conflict_mode, extension_target): + """ + Core method to rename the file extensions. + """ + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + for file_path in list_files: + num = 1 + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + file_ext = os.path.splitext(file_name)[1] + + if file_ext == "": + list_skipped.append(file_path) + continue + + file_newpath = file_path.replace(file_ext, + os.path.extsep + extension_target) + if file_path == file_newpath: + list_skipped.append(file_path) + continue + + if conflict_mode == "rename": + while True: + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if file_path.lower() == file_newpath.lower(): + break + file_newpath = file_path.replace(file_ext, "_" + + str(num) + + os.path.extsep + + extension_target) + num += 1 + else: + break + elif conflict_mode == "skip": + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if not file_path.lower() == file_newpath.lower(): + list_skipped.append(file_path) + continue + else: + list_skipped.append(file_path) + continue + + if os.path.exists(file_path): + list_renamed.append([file_path, file_newpath + ".__temp__", + file_newpath]) + + return list_renamed, list_skipped + +# EOF + diff --git a/python2/core/fileren.py b/python2/core/fileren.py new file mode 100755 index 0000000..5dd51b9 --- /dev/null +++ b/python2/core/fileren.py @@ -0,0 +1,838 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File renamer core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +import common +import os +import paval as pv +import re +import statcase + +from datetime import datetime as dt + +def convert_case(directory, case, conflict_mode, recursive=False, + cfg_lower=None, cfg_mixed=None, cfg_title=None, + cfg_upper=None, report_file=None, ignore_symlinks=False): + """ + Convert the case of the base name of files. + """ + pv.path(directory, "given", False, True) + pv.compstr(case, "case", ["lower", "title", "upper", "config"]) + pv.compstr(conflict_mode, "conflict mode", ["rename", "skip"]) + + case = case.lower() + conflict_mode = conflict_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = re.compile("(.*)") + + __check_config_mismatch(case, cfg_lower, cfg_title, cfg_mixed, cfg_upper) + list_content, list_excluded = \ + common.get_files(directory, recursive, True, regex, False, + ignore_symlinks) + + for item in list_content: + list_files = item[1] + list_renamed, list_skipped = \ + __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive, cfg_lower, cfg_mixed, + cfg_title, cfg_upper) + + if simulate: + list_header = [] + list_header.append("Nomen File Name Case Converter simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Conflict mode:", conflict_mode.capitalize()]) + list_header.append(["Case:", case.capitalize()]) + list_header.append(["Lowercase config:", cfg_lower]) + list_header.append(["Mixed case config:", cfg_mixed]) + list_header.append(["Title case config:", cfg_title]) + list_header.append(["Uppercase config:", cfg_upper]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def modify_names(directory, action, position, input_string, + replace_string=None, recursive=False, exclude=None, + pattern=None, ignore_case=True, regex_syntax=False, + report_file=None, ignore_symlinks=False, strip_chars=None): + """ + Modify the base name of files by adding, removing or replacing a + user-defined string. + """ + pv.path(directory, "given", False, True) + pv.compstr(action, "action", ["add", "remove", "replace"]) + pv.compstr(position, "position", ["any", "prefix", "suffix"]) + pv.string(input_string, "input string", False, common.get_invalid_chars()) + + action = action.lower() + position = position.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + if not replace_string == None: + if not action == "replace": + raise Exception("The replace string argument can only be used " \ + "together with the action 'replace'.") + else: + pv.string(replace_string, "string False", False, + common.get_invalid_chars()) + + if action == "add" and position == "any": + raise Exception("The position 'any' cannot be used together with " \ + "the action 'add'.") + + if len(input_string) == 0: + raise Exception("The input string must not be empty.") + else: + pv.string(input_string, "input string", False, + common.get_invalid_chars()) + + if not strip_chars == None: + pv.string(strip_chars, "strip chars string", False, + common.get_invalid_chars()) + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = None + if not pattern == None: + regex = common.compile_regex(pattern, ignore_case, regex_syntax) + + list_content, list_excluded = \ + common.get_files(directory, recursive, ignore_case, regex, exclude, + ignore_symlinks) + for item in list_content: + list_files = item[1] + __modify_names(list_files, list_renamed, list_skipped, action, + position, input_string, replace_string, strip_chars) + + if simulate: + explicit = None + if exclude == None: + exclude = False + explicit = False + elif exclude: + explicit = False + else: + explicit = True + + list_header = [] + list_header.append("Nomen File Name Modifier simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Action to perform:", action.capitalize()]) + list_header.append(["Position:", position.capitalize()]) + list_header.append(["Input string:", "\"" + input_string + "\" " \ + "(without double quotes)"]) + if not replace_string == None: + list_header.append(["Replace string:", "\"" + replace_string + \ + "\" (without double quotes)"]) + if strip_chars == None: + list_header.append(["Strip chars:", "None"]) + else: + list_header.append(["Strip chars:", "\"" + strip_chars + "\" " \ + "(without double quotes)"]) + + list_header.append(["Exclude files:", exclude]) + list_header.append(["Explicit files:", explicit]) + list_header.append(["Pattern:", pattern]) + list_header.append(["Ignore case:", ignore_case]) + list_header.append(["Regex syntax:", regex_syntax]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def rename_files(directory, rename_mode, separator=" ", recursive=False, + padding=0, exclude=None, pattern=None, ignore_case=True, + regex_syntax=False, report_file=None, ignore_symlinks=False, + ignore_file_ext=False, custom_name=None, step=1, + order_by=None): + """ + Rename the base name of files based on the name of the directory where + they are stored in and add a numeric ID. + """ + pv.path(directory, "given", False, True) + pv.compstr(rename_mode, "rename mode", + ["fill-gaps", "increase", "keep-order", "rename-new"]) + pv.intrange(padding, "padding", 0, 12, True) + pv.string(separator, "seperator", False, common.get_invalid_chars()) + pv.intvalue(step, "step", True, False, False) + + if not order_by == None: + pv.compstr(order_by, "order by", ["accessed", "created", "modified"]) + if not rename_mode == "keep-order": + raise Exception("The order-by argument can only be used in " \ + "combination with keep-order mode.") + + step = int(step) + rename_mode = rename_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + if not custom_name == None: + pv.string(custom_name, "custom file name", False, + common.get_invalid_chars()) + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = None + if not pattern == None: + regex = common.compile_regex(pattern, ignore_case, regex_syntax) + + list_content, list_excluded = \ + common.get_files(directory, recursive, ignore_case, regex, exclude, + ignore_symlinks, order_by) + + for item in list_content: + list_files = item[1] + if rename_mode == "fill-gaps": + list_renamed, list_skipped = \ + __rename_files_fill(list_files, list_renamed, list_skipped, + separator, padding, True, ignore_file_ext, + custom_name, step) + elif rename_mode == "rename-new": + list_renamed, list_skipped = \ + __rename_files_fill(list_files, list_renamed, list_skipped, + separator, padding, False, + ignore_file_ext, custom_name, step) + elif rename_mode == "keep-order": + list_renamed, list_skipped = \ + __rename_files_keep_order(list_files, list_renamed, + list_skipped, separator, padding, + ignore_file_ext, custom_name, step, + order_by) + else: + raise Exception("An invalid rename mode was given.") + + if simulate: + if padding == 0: + padding = "Set automatically" + else: + padding = str(padding) + + explicit = None + if exclude == None: + exclude = False + explicit = False + elif exclude: + explicit = False + else: + explicit = True + + if order_by == "accessed": + order_by = "Access time" + elif order_by == "created": + order_by = "Creation time" + elif order_by == "modified": + order_by = "Modification time" + else: + order_by = "False" + + list_header = [] + list_header.append("Nomen File Renamer simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Rename mode:", rename_mode.capitalize()]) + list_header.append(["Order by time:", order_by]) + list_header.append(["Separator:", "\"" + separator + "\" " \ + "(without double quotes)"]) + list_header.append(["Numeric padding:", padding]) + list_header.append(["Step size:", step]) + list_header.append(["Exclude files:", exclude]) + list_header.append(["Explicit files:", explicit]) + list_header.append(["Pattern:", pattern]) + list_header.append(["Ignore case:", ignore_case]) + list_header.append(["Regex syntax:", regex_syntax]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def __check_config_mismatch(case, cfg_lower, cfg_title, cfg_mixed, cfg_upper): + """ + Check for a case config mismatch. + """ + cfg_mismatch = False + + if case == "lower": + if not cfg_lower == None: + cfg_mismatch = True + elif case == "title": + if not cfg_title == None: + cfg_mismatch = True + elif case == "upper": + if not cfg_upper == None: + cfg_mismatch = True + elif case == "config": + if cfg_lower == None and \ + cfg_mixed == None and \ + cfg_title == None and \ + cfg_upper == None: + raise Exception("The config target case requires at least one " \ + "case config file to operate.") + else: + raise Exception("An unsupported case string was given.") + + if cfg_mismatch: + if case == "title": + case += " " + raise Exception("Config file mismatch (cannot use %scase config " \ + "file when using %scase as target case anyway)." % \ + (case, case)) + +def __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive, cfg_lower, cfg_mixed, cfg_title, + cfg_upper): + """ + Core method to convert the case of the base name of files. + """ + file_ext = "" + list_lower = [] + list_mixed = [] + list_title = [] + list_upper = [] + + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + if cfg_lower == None and cfg_mixed == None and \ + cfg_title == None and cfg_upper == None: + static_case = False + else: + list_lower, list_mixed, list_title, list_upper = \ + statcase.parse_case_configs(cfg_lower, cfg_mixed, cfg_title, + cfg_upper) + static_case = True + + for file_path in list_files: + num = 1 + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + base_name = os.path.splitext(file_name)[0] + if case == "lower": + base_name_target = base_name.lower() + elif case == "title": + base_name_target = base_name.title() + elif case == "upper": + base_name_target = base_name.upper() + else: + base_name_target = base_name + + if static_case: + base_name_target = __static_case(base_name_target, case, + list_lower, list_mixed, + list_title, + list_upper).rstrip() + + file_newpath = file_path.replace(base_name + file_ext, + base_name_target + file_ext) + if file_path == file_newpath: + list_skipped.append(file_path) + continue + + if conflict_mode == "rename": + while True: + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if file_path.lower() == file_newpath.lower(): + break + file_newpath = \ + file_path.replace(base_name, + base_name_target + "_" + str(num)) + num += 1 + else: + break + elif conflict_mode == "skip": + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if not file_path.lower() == file_newpath.lower(): + list_skipped.append(file_path) + continue + else: + list_skipped.append(file_path) + continue + + if os.path.exists(file_path): + list_renamed.append([file_path, file_newpath + ".__temp__", + file_newpath]) + + return list_renamed, list_skipped + +def __fill_num_gaps(list_files, separator, padding, list_renamed, + list_skipped, fs_case, step): + """ + Core method to fill numeration gaps. + """ + list_temp = [] + list_temp.extend(list_skipped) + + for i in list_renamed: + list_temp.append(i[2]) + list_temp.sort() + list_gaps = __get_num_gaps(list_files, separator, padding, step) + + if len(list_gaps) > 0: + list_gaps.sort(reverse=True) + list_skipped.sort(reverse=True) + + while len(list_gaps) > 0: + if len(list_skipped) < 1: + break + + file_path = list_skipped.pop(0) + list_path = file_path.split(os.path.sep) + file_dir = list_path[-2] + file_name = list_path[-1] + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + num = list_gaps.pop(0) + file_num = str(num).rjust(int(padding), "0") + file_newname = file_dir + separator + \ + file_num.replace(" ", "0") + file_ext + file_newpath = file_path.replace(file_name, file_newname) + + if common.file_exists(file_newpath, list_renamed, fs_case): + list_skipped.append(file_path) + else: + list_renamed.append([file_path, None, file_newpath]) + + return list_renamed, list_skipped + +def __get_num_gaps(list_files, separator, padding, step): + """ + Method to determine numeration gaps. + """ + list_gaps = [] + + for i in range(len(list_files)): + x = (i + 1) * step + n = separator + str(x).rjust(int(padding),"0") + if not any(n in s for s in list_files): + list_gaps.append(int(n.replace(separator, ""))) + + return list_gaps + +def __modify_name_add(file_name, string, position): + """ + Core method to add a string to the base name of a file. + """ + file_newname = "" + + if position == "prefix": + file_newname = string + file_name + elif position == "suffix": + file_newname = file_name + string + + return file_newname + +def __modify_name_remove(file_name, string, position): + """ + Core method to remove a string from the base name of a file. + """ + file_newname = "" + + if position == "any": + file_newname = file_name.replace(string, "") + elif position == "prefix": + file_newname = re.sub("^" + string, "", file_name) + elif position == "suffix": + file_newname = re.sub(string + "$", "", file_name) + + return file_newname + +def __modify_name_replace(file_name, string, replace_string, position): + """ + Core method to replace a string inside the base name of a file. + """ + file_newname = "" + + if position == "any": + file_newname = file_name.replace(string, replace_string) + elif position == "prefix": + file_newname = re.sub("^" + string, replace_string, file_name) + elif position == "suffix": + file_newname = re.sub(string + "$", replace_string, file_name) + + return file_newname + +def __modify_names(list_files, list_renamed, list_skipped, action, position, + input_string, replace_string, strip_chars): + """ + Core method to modify the base name of files by adding, removing or + replacing a user-defined string. + """ + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + file_newname = "" + file_newpath = "" + + if os.path.extsep in file_name: + file_temp = os.path.splitext(file_name) + file_name = file_temp[0] + file_ext = file_temp[1] + else: + file_ext = "" + + if action == "add": + file_newname = __modify_name_add(file_name, input_string, + position) + elif action == "remove": + file_newname = __modify_name_remove(file_name, input_string, + position) + elif action == "replace": + file_newname = __modify_name_replace(file_name, input_string, + replace_string, position) + + if not strip_chars == None: + if len(strip_chars) > 0: + file_newname = file_newname.strip(strip_chars) + + file_newname += file_ext + file_newpath = file_path.replace(file_name + file_ext, file_newname) + if file_newpath == "": + list_skipped.append(file_path) + elif file_newname == "" or file_newname == file_ext: + list_skipped.append(file_path) + else: + if file_path == file_newpath: + list_skipped.append(file_path) + else: + if os.path.exists(file_newpath): + list_skipped.append(file_path) + else: + list_renamed.append([file_path, None, file_newpath]) + + return list_renamed, list_skipped + +def __process_case_list(case, case_list): + """ + Process a case list and separate words from regular expressions. + """ + list_strings = [] + list_words = [] + + for item in case_list: + if case == "lower": + if item.startswith("$("): + list_strings.append(item.replace("$", "").lower()) + else: + list_words.append(item.lower()) + elif case == "title": + if item.startswith("$("): + list_strings.append(item.replace("$", "").title()) + else: + list_words.append(item.title()) + elif case == "upper": + if item.startswith("$("): + list_strings.append(item.replace("$", "").upper()) + else: + list_words.append(item.upper()) + else: + if item.startswith("$("): + list_strings.append(item.replace("$", "")) + else: + list_words.append(item) + + return list_words, list_strings + +def __rename_files_fill(list_files, list_renamed, list_skipped, separator, + padding, fill_gaps=False, ignore_file_ext=False, + custom_name=None, step=1): + """ + Core method to rename the base name of files based on the name of the + directory where they are stored in using one of the "fill" rename + modes (such as "fill-gaps" and "rename-new"). + """ + file_newpath = "" + num = 0 + + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + if fill_gaps: + list_temp_renamed = [] + list_temp_skipped = [] + obj_ren = list_temp_renamed + obj_skip = list_temp_skipped + else: + obj_ren = list_renamed + obj_skip = list_skipped + + if padding == 0: + padding = len(str(len(list_files))) + + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + + if custom_name == None: + file_dir = list_path[-2] + else: + file_dir = custom_name + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + if file_name.startswith(file_dir + separator): + try: + temp = file_name.replace(file_dir + separator, "") + list_pad = temp.split(".") + file_padding = len(list_pad[0]) + + if step > 1: + if int(list_pad[0]) % step == 0: + obj_skip.append(file_path) + continue + else: + if int(padding) == file_padding: + obj_skip.append(file_path) + continue + except: + pass + + if not ignore_file_ext: + num = 0 + + file_newpath = file_path + while common.file_exists(file_newpath, obj_ren, fs_case) or \ + common.file_exists(file_newpath, obj_skip, fs_case): + num += step + file_num = str(num).rjust(int(padding), "0") + file_newname = \ + file_dir + separator + file_num.replace(" ", "0") + file_ext + file_newpath = file_path.replace(file_name, file_newname) + + if os.path.exists(file_path): + if file_path == file_newpath: + obj_skip.append(file_path) + else: + obj_ren.append([file_path, None, file_newpath]) + + if fill_gaps: + list_temp_renamed, list_temp_skipped = \ + __fill_num_gaps(list_files, separator, padding, + list_temp_renamed, list_temp_skipped, + fs_case, step) + list_renamed.extend(list_temp_renamed) + list_skipped.extend(list_temp_skipped) + + return list_renamed, list_skipped + +def __rename_files_keep_order(list_files, list_renamed, list_skipped, + separator, padding, ignore_file_ext=False, + custom_name=None, step=1, order_by=None): + """ + Core method to rename the base name of files based on the name of the + directory where they are stored in using "keep-order" rename mode. + """ + file_newpath = "" + file_temppath = "" + temp_file_ext = "" + list_new = [] + list_ren = [] + num = 0 + + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + if padding == 0: + padding = len(str(len(list_files))) + + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_dir = list_path[-2] + file_name = list_path[-1] + + if file_name.startswith(file_dir + separator): + list_ren.append(file_path) + else: + list_new.append(file_path) + + list_files = [] + list_files.extend(list_ren) + list_files.extend(list_new) + + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + + if custom_name == None: + file_dir = list_path[-2] + else: + file_dir = custom_name + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + if not ignore_file_ext: + if not file_ext == temp_file_ext: + num = 0 + + file_temppath = file_path + temp_file_ext = file_ext + while common.file_exists(file_temppath, list_renamed, fs_case): + num += step + file_num = str(num).rjust(int(padding), "0") + file_newname = \ + file_dir + separator + file_num.replace(" ", "0") + file_ext + file_newpath = file_path.replace(file_name, file_newname) + if not file_newpath in list_skipped: + file_temppath = file_newpath + ".__temp__" + + if os.path.exists(file_path): + if file_path == file_newpath: + list_skipped.append(file_path) + else: + list_renamed.append([file_path, file_temppath, file_newpath]) + + return list_renamed, list_skipped + +def __static_case(base_name, case, list_lower, list_mixed, list_title, + list_upper): + """ + Convert the case of the base name of a file to lowercase, mixed case, + title case or uppercase based on the read out config files. + """ + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("lower", list_lower) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("mixed", list_mixed) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("title", list_title) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("upper", list_upper) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + return base_name + +def __static_case_word(list_cfg_str, list_file_str): + """ + Convert the case of each word of the base name of a file. + """ + base_name = "" + + for file_str in list_file_str: + for cfg_str in list_cfg_str: + spec_chars = "\\?*+$.:; ^|()[]{}" + for char in spec_chars: + cfg_str = cfg_str.replace(char, "") + + if file_str.lower() == cfg_str.lower(): + file_str = cfg_str + + base_name += (file_str + " ") + + return base_name + +def __static_case_string(list_cfg_regex, base_name): + """ + Convert the case of a string inside the base name of a file. + """ + for item in list_cfg_regex: + spec_chars = "\\?*+$.:;^|()[]{}" + for char in spec_chars: + item = item.replace(char, "") + + regex = re.compile(".*" + item.lower() + ".*") + if regex.match(base_name.lower()): + base_name = re.sub(item.lower(), item, base_name, + flags=re.IGNORECASE) + + return base_name + +# EOF + diff --git a/python2/core/paval.py b/python2/core/paval.py new file mode 100755 index 0000000..3138fcb --- /dev/null +++ b/python2/core/paval.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# PaVal - Parameter validation module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/paval +# ============================================================================ + +__version__ = "1.2.7" + +import filecmp +import os + + +def compfile(input_path, name="", list_files=None): + """ + Compare files to avoid that the same file is given multiple times or + in different ways (e. g. different name but same content). + """ + __string(input_path, "%s path" % name, True) + + if list_files is None: + list_files = [] + elif not list_files: + __ex("File list is empty (no files to compare with).", True) + else: + for item in list_files: + if not isinstance(item, list): + __ex("Every list item must be a sub-list.", True) + if not len(item) == 2: + __ex("Every sub-list must contain two items.", True) + + input_path = os.path.abspath(input_path) + for item in list_files: + path_compare = os.path.abspath(str(item[0])) + name_compare = str(item[1]) + if input_path == path_compare: + __ex("The %s and the %s file path must not be identical." % + (name, name_compare), False) + if os.path.exists(input_path) and os.path.exists(path_compare): + if filecmp.cmp(input_path, path_compare, 0): + __ex("The %s and %s file content must not be identical." % + (name, name_compare), False) + + +def compstr(input_string, name="", list_strings=None): + """ + Compare a string with a list of strings and check if it is an item of + that list. + """ + __string(input_string, name, False) + if not list_strings: + __ex("No %s strings to compare with." % name, True) + if input_string not in list_strings: + __ex("The %s '%s' does not exist." % (name, input_string), False) + + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + + +def intrange(value, name="", value_min=None, value_max=None, zero=False): + """ + Validate an integer range. + """ + value = __integer(value, "%s value" % name, False) + if value_min is not None: + value_min = __integer(value_min, "minimal %s value" % name, True) + intvalue(value_min, name, True, True, True) + if value_max is not None: + value_max = __integer(value_max, "maximal %s value" % name, True) + intvalue(value_max, name, True, True, True) + if not zero: + if value == 0: + __ex("The %s value must not be zero." % name, False) + if (value_min is not None) and (value_max is not None): + if value_min > value_max: + __ex("The maximal %s value must be greater than the minimal " + "value." % name, False) + if (value_min == value_max) and (value != value_min): + __ex("The %s value can only be %s (depending on further range " + "further range arguments)." % (name, value_min), False) + if (value < value_min) or (value > value_max): + __ex("The %s value must be between %s and %s (depending on " + "further range arguments)." % (name, value_min, value_max), + False) + elif value_min is not None: + if value < value_min: + __ex("The %s value must not be less than %s." % (name, value_min), + False) + elif value_max is not None: + if value > value_max: + __ex("The %s value must not be greater than %s." % + (name, value_max), False) + + +def intvalue(value, name="", positive=True, zero=False, negative=False): + """ + Validate a single integer value. + """ + value = __integer(value, "%s value" % name, False) + if not positive: + if value > 0: + __ex("The %s value must not be positive." % name, False) + if not zero: + if value == 0: + __ex("The %s value must not be zero." % name, False) + if not negative: + if value < 0: + __ex("The %s value must not be negative." % name, False) + + +def path(input_path, name="", is_file=False, exists=False): + """ + Validate a path of a file or directory. + """ + string(input_path, "%s path" % name, False, None) + input_path = os.path.abspath(input_path) + + if is_file: + path_type = "file" + else: + path_type = "directory" + if exists: + if not os.path.exists(input_path): + __ex("The given %s %s does not exist." % (name, path_type), False) + if (is_file and not os.path.isfile(input_path)) or \ + (not is_file and not os.path.isdir(input_path)): + __ex("The given %s %s path is not a %s." % (name, path_type, + path_type), False) + else: + if os.path.exists(input_path): + __ex("The given %s %s path already exists." % (name, path_type), + False) + + +def string(input_string, name="", wildcards=False, invalid_chars=None): + """ + Validate a string. + """ + __string(input_string, name, False) + if invalid_chars is None: + invalid_chars = "" + if not wildcards: + if ("*" in input_string) or ("?" in input_string): + __ex("The %s must not contain wildcards." % name, False) + if invalid_chars: + for char in invalid_chars: + if char in input_string: + # Use single quotes by default or double quotes in case the + # single quotes are the invalid character + quotes = "'" + if char == quotes: + quotes = "\"" + + __ex("The %s contains at least one invalid character " + "(%s%s%s)." % (name, quotes, char, quotes), False) + + +def __ex(exception_string, internal=False): + """ + Internal method to raise an exception. + """ + ex = str(exception_string).strip() + while " " * 2 in ex: + ex = ex.replace((" " * 2), " ") + if internal: + ex = "PaVal: " + ex + raise Exception(ex) + + +def __integer(value, name="", internal=False): + """ + Internal method for basic integer validation. + """ + if value is None: + __ex("The %s is missing." % name, internal) + if value == "": + __ex("The %s must not be empty." % name, internal) + try: + value = int(value) + except ValueError: + __ex("The %s must be an integer." % name, internal) + return int(value) + + +def __string(input_string, name="", internal=False): + """ + Internal method for basic string validation. + """ + if input_string is None: + __ex("The %s is missing." % name, internal) + if input_string == "": + __ex("The %s must not be empty." % name, internal) + +# EOF diff --git a/python2/core/statcase.py b/python2/core/statcase.py new file mode 100755 index 0000000..656da7e --- /dev/null +++ b/python2/core/statcase.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Static case core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +import os +import paval as pv +import sys + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def parse_case_configs(cfg_lower=None, cfg_mixed=None, cfg_title=None, + cfg_upper=None): + """ + Parse the configuration files for static file name case + conversion. + """ + list_lower = [] + list_mixed = [] + list_title = [] + list_upper = [] + + if not cfg_lower == None: + list_lower = __read_config(__config_abspath(cfg_lower, "lowercase")) + if not cfg_mixed == None: + list_mixed = __read_config(__config_abspath(cfg_mixed, "mixed case")) + if not cfg_title == None: + list_title = __read_config(__config_abspath(cfg_title, "title case")) + if not cfg_upper == None: + list_upper = __read_config(__config_abspath(cfg_upper, "uppercase")) + + __check_dupes(list_lower, list_mixed, list_title, list_upper) + + return list_lower, list_mixed, list_title, list_upper + +def __check_dupes(list_lower, list_mixed, list_title, list_upper): + """ + Check for duplicate case list entries. + """ + list_diff = [] + + list_diff = __process_case_list(list_lower, list_diff) + list_diff = __process_case_list(list_mixed, list_diff) + list_diff = __process_case_list(list_title, list_diff) + __process_case_list(list_upper, list_diff) + +def __config_abspath(config, description): + """ + Get the absolute path of a config file. + """ + description += " config" + try: + pv.path(config, description, True, True) + except: + config_dir = os.path.abspath(os.path.dirname(sys.argv[0])) + config = os.path.join(config_dir, "cfg", os.path.basename(config)) + pv.path(config, description, True, True) + config = os.path.abspath(config) + + return config + +def __process_case_list(list_input, list_output): + """ + Process a case list and raise an exception in case of duplicate + entries. + """ + duplicate = "" + + for item in list_input: + if item == None or item == "": + continue + elif item.lower() in list_output: + duplicate = item + break + else: + list_output.append(item.lower()) + + if not duplicate == "": + raise Exception("Duplicate config file entries. The same string " \ + "must not exist in multiple config files. The " \ + "duplicate string was \"%s\" (without the " \ + "enclosing quotes)." % duplicate) + + return list_output + +def __read_config(config_file): + """ + Read out the contents of a config file. + """ + list_config = [] + + fh_config = open(config_file, "r") + for line in fh_config: + line = line.strip() + if line == "" or line.startswith("#"): + continue + list_config.append(line.replace("\n", "")) + fh_config.close() + list_config.sort + + return list_config + +# EOF + diff --git a/python2/docs/usage_nomen-dirspace.txt b/python2/docs/usage_nomen-dirspace.txt new file mode 100644 index 0000000..a91c061 --- /dev/null +++ b/python2/docs/usage_nomen-dirspace.txt @@ -0,0 +1,195 @@ + +USAGE (nomen-dirspace.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 2.5 Identical directory names + 3. Remove spaces from directory names + 3.1 Duplicate spaces + 3.2 Leading spaces + 3.3 Trailing spaces + 3.4 Duplicate, leading as well as trailing spaces + 4. Spaces next to certain characters + 4.1 Brackets + 4.2 Hyphens + 4.3 Punctuation characters + + 1. Definition + + The Nomen Directory Name Space Modifier (former "Nomen Directory Name + Space Remover") script modifies a directory name by removing leading, + trailing and duplicate spaces or by inserting and removing spaces next + to punctuation characters. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-dirspace.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + As a matter of fact, there is no function to undo the changes done + by this tool, so you should be aware of what you are doing. If + not, do NOT use this tool. + + Improper use (e. g. modifying system directories) will corrupt + your system! + + 2.4 Simulation mode + + Due to the fact that this tool simply removes unnecessary spaces + in directory names, it does not provide any simulation mode. + + 2.5 Identical directory names + + For example, if you have the directories + + /tmp/pics/My Holiday/ + /tmp/pics/My Holiday/ + + and want to remove the duplicate spaces, both directories would + have the same name after performing the space removal operation. + Due to this, they would be combined and the data inside of them + would get mixed up. + + To avoid this, the Nomen Directory Name Space Remover script does + not remove the spaces from a directory if another directory with + the new name already exists. The directory will simply be ignored, + but the script will process its sub-directories if the recursive + command-line argument was given. + + 3. Remove spaces from directory names + + The following operations can also be combined using the corresponding + command-line arguments (as described in section 3.4). + + 3.1 Duplicate spaces + + For example, if you have the following directory + + /tmp/pics/My Holiday/ + + and you want to remove the duplicate spaces, type: + + ./nomen-dirspace.py -d /tmp/pics/ -s + + 3.2 Leading spaces + + For example, if you have the following directory + + /tmp/pics/ My Holiday/ + + and you want to remove the leading spaces, type: + + ./nomen-dirspace.py -d /tmp/pics/ -l + + 3.3 Trailing spaces + + For example, if you have the following directory + + /tmp/pics/My Holiday / + + and you want to remove the trailing spaces, type: + + ./nomen-dirspace.py -d /tmp/pics/ -t + + 3.4 Duplicate, leading as well as trailing spaces + + For example, if you have the following directory + + /tmp/pics/ My Holiday / + + and you want to remove the duplicate, leading and trailing spaces, + type: + + ./nomen-dirspace.py -d /tmp/pics/ -s -l -t + + 4. Spaces next to certain characters + + The Nomen Directory Name Space Modifier also allows inserting and + removing spaces next to certain characters such as brackets, dashes + and punctuation characters. These are automated processes without any + further options. + + The following command-line arguments can be combined with those + mentioned in section 3 as well as with each other. + + Please notice that inserting spaces may result in duplicate spaces + again. So, when using the following command-line arguments it is + recommended to also add the "-s" (or "--remove-duplicate") argument + (as shown in the examples below). + + 4.1 Brackets + + The "-b" (or "--brackets") command-line argument will remove all + unnecessary spaces around brackets. For example, if you have a + directory like this + + /tmp/music/ Foo( Bar [ Album ] ) / + + and run Nomen Directory Name Space Modifier with the corresponding + argument + + ./nomen-dirspace.py -d /tmp/music/ -s -b + + the directory will be changed as follows: + + /tmp/music/Foo (Bar [Album])/ + + 4.2 Hyphens + + The --hyphens" command-line argument will add additional spaces + around hyphens ("-"). + + /tmp/music/ John Doe -Foo / + + and run Nomen Directory Name Space Modifier with the corresponding + argument + + ./nomen-dirspace.py -d /tmp/music/ -s -p + + the directory will be changed as follows: + + /tmp/music/John Doe - Foo/ + + 4.3 Punctuation characters + + The "-p" (or "--punctuation") command-line argument will remove + all unnecessary spaces around the punctuation characters such as + dots ("."), commas (","), colons (":"), semi-colons (";") as well + as exclamation ("!") and question marks ("?"). Furthermore, it + adds spaces next to them when necessary. + + /tmp/music/ Foo,Bar feat.John Doe - Foo ! / + + and run Nomen Directory Name Space Modifier with the corresponding + argument + + ./nomen-dirspace.py -d /tmp/music/ -s -p + + the directory will be changed as follows: + + /tmp/music/Foo, Bar feat. John Doe - Foo!/ + diff --git a/python2/docs/usage_nomen-extcase.txt b/python2/docs/usage_nomen-extcase.txt new file mode 100644 index 0000000..f75c612 --- /dev/null +++ b/python2/docs/usage_nomen-extcase.txt @@ -0,0 +1,126 @@ + +USAGE (nomen-extcase.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 3. Convert the case of file extensions + 3.1 Rename mode + 3.2 Skip mode + + 1. Definition + + The Nomen Extension Case Converter script converts the case of the + extension of all files inside a directory and (if requested) in all of + its sub-directories. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-extcase.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-extcase.py -m rename -c lower -d /tmp/pics/Holidays + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-extcase.py -m rename -c lower -d /tmp/pics/Holidays \ + --simulate /tmp/report.txt + + 3. Convert the case of file extensions + + For example, you have two picture files inside a directory called + "Holidays" with these extensions + + Holidays 1.JPG + Holidays 2.Jpg + + and you convert the extension case to lowercase, the file extensions + will look like this: + + Holidays 1.jpg + Holidays 2.jpg + + But, for example, if file system is case senstive and the directory + contains the files + + Holidays 1.JPG + Holidays 1.Jpg + + there would be a problem converting the file extension case, because + both target file names would be identical. So, to avoid that, the + script also offers two operating modes. + + 3.1 Rename mode + + The "rename mode" adds a numeric ID to the duplicate target file + and converts the file extension: + + Holidays 1.JPG will be renamed to "Holidays 1.jpg" + Holidays 1.Jpg will be renamed to "Holidays 1_1.jpg" + + To use this mode and convert the file extensions to lowercase, + type: + + ./nomen-extcase.py -m rename -c lower -d /tmp/pics/Holidays + + 3.2 Skip mode + + The "skip mode" simply skips the files where converting the case + of the file extension would result in duplicate file names: + + Holidays 1.JPG will be renamed to "Holidays 1.jpg" + Holidays 1.Jpg will be skipped + + To use this mode and convert the file extensions to lowercase, + type: + + ./nomen-extcase.py -m skip -c lower -d /tmp/pics/Holidays + diff --git a/python2/docs/usage_nomen-extren.txt b/python2/docs/usage_nomen-extren.txt new file mode 100644 index 0000000..33719ea --- /dev/null +++ b/python2/docs/usage_nomen-extren.txt @@ -0,0 +1,134 @@ + +USAGE (nomen-extren.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 3. Rename file extensions + 3.1 Rename mode + 3.2 Skip mode + + 1. Definition + + The Nomen Extension Renamer script renames (and adjusts) differently + spelled file extensions of the same file type file within a directory + and (if requested) in all of its sub-directories. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-extren.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-extren.py -m rename -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-extren.py -m rename -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" \ + --simulate /tmp/report.txt + + 3. Rename file extensions + + For example, you have four picture files inside a directory called + "Holidays" with these extensions: + + Holidays 1.jpg + Holidays 2.jpeg + Holidays 3.JPG + Holidays 4.Jpeg + + All of these files are JPEG pictures, but their extensions are spelled + differently. So, the Nomen Extension Renamer script can be used to get + consistent file extensions: + + Holidays 1.jpg + Holidays 2.jpg + Holidays 3.jpg + Holidays 4.jpg + + But, for example, if file system is case senstive and the directory + contains the files + + Holidays 1.Jpg + Holidays 1.jpeg + + there would be a problem renaming the file extension, because both + file target names would be identical. So, to avoid that, the script + also offers two operating modes. + + 3.1 Rename mode + + The "rename mode" adds a numeric ID to the duplicate target file + and converts the file: + + Holidays 1.Jpg will be renamed to "Holidays 1.jpg" + Holidays 1.jpeg will be renamed to "Holidays 1_1.jpg" + + To use this mode and rename the extensions of JPEG files, + type: + + ./nomen-extren.py -m rename -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" + + 3.2 Skip mode + + The "skip mode" simply skips the files where converting the case + of the file extension would result in duplicate file names: + + Holidays 1.Jpg will be renamed to "Holidays 1.jpg" + Holidays 1.jpeg will be skipped + + To use this mode and rename the extensions of JPEG files, type: + + ./nomen-extren.py -m skip -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" + diff --git a/python2/docs/usage_nomen-filecase.txt b/python2/docs/usage_nomen-filecase.txt new file mode 100644 index 0000000..2af0fdc --- /dev/null +++ b/python2/docs/usage_nomen-filecase.txt @@ -0,0 +1,186 @@ + +USAGE (nomen-filecase.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 3. Convert the case of base names + 3.1 Rename mode + 3.2 Skip mode + 4. Using config files + 4.1 With a certain target case + 4.2 Without a certain target case + + 1. Definition + + The Nomen File Name Case Converter script converts the case of the + base name of all files inside a directory and (if requested) in all of + its sub-directories. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-filecase.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-filecase.py -m rename -c title -d /tmp/pics/Holidays + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-filecase.py -m rename -c title -d /tmp/pics/Holidays \ + --simulate /tmp/report.txt + + 3. Convert the case of base names + + For example, you have a picture file with this file name + + my favorite holiday.jpg + + and you convert the case of the base name to title case, the file name + will look like this: + + My Favorite Holiday.jpg + + But, for example, if file system is case senstive and the directory + contains the files + + HOLIDAY.jpg + holiday.jpg + + there would be a problem converting the case of the base name, because + both target file names would be identical. So, to avoid that, the + script also offers two operating modes. + + 3.1 Rename mode + + The "rename mode" adds a numeric ID to the base name of the + duplicate target file and converts the case: + + HOLIDAY.jpg will be renamed to "Holiday.jpg" + holiday.jpg will be renamed to "Holiday_1.jpg" + + To use this mode and convert the base name of the files to title + case, type: + + ./nomen-filecase.py -m rename -c title -d /tmp/pics/Holidays + + 3.2 Skip mode + + The "skip mode" simply skips the files where converting the case + of the base name would result in duplicate file names: + + HOLIDAY.jpg will be renamed to "Holiday.jpg" + holiday.jpg will be skipped + + To use this mode and convert the base name of the files to title + case, type: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays + + 4. Using config files + + The Nomen File Name Case Converter script also comes with the feature + that allows using config files to convert the case of certain strings + always to lowercase, title case, uppercase or a user-defined mixed + case, nomatter which target case (using the "-c" or "--case" argument) + was given. + + For details see the included sample config files which can be found + inside the "cfg" sub-directory. + + If the sample config file is missing for some reason, you may download + the project from the website: + + http://www.urbanware.org/nomen.html + + To use these config files, the corresponding command-line argument + followed by the full path to the config file is required, depending on + which of the config files you want to use. + + 4.1 With a certain target case + + For example, if you want to use title case as target case as well + as the lowercase config file named "lower.cfg" which is located + inside the directory "/tmp", the command line would look like + this: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg + + To additionally use the uppercase config file "upper.cfg" which + is also located inside the "/tmp" directory, simply add the + appropriate argument: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg \ + --cfg-upper /tmp/upper.cfg + + Same with the mixed case config: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg \ + --cfg-mixed /tmp/mixed.cfg \ + --cfg-upper /tmp/upper.cfg + + However, you cannot use the title case config file when using + title case as target case anyway. + + 4.2 Without a certain target case + + If you just want to convert the case of the certain strings stored + inside the case config files, you can do this by giving "config" + as the target case: + + ./nomen-filecase.py -m skip -c config -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg \ + --cfg-mixed /tmp/mixed.cfg \ + --cfg-title /tmp/title.cfg \ + --cfg-upper /tmp/upper.cfg + diff --git a/python2/docs/usage_nomen-filemod.txt b/python2/docs/usage_nomen-filemod.txt new file mode 100644 index 0000000..5de39cd --- /dev/null +++ b/python2/docs/usage_nomen-filemod.txt @@ -0,0 +1,376 @@ + +USAGE (nomen-filemod.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 2.5 Prefixes and suffixes + 3. Add a user-defined string to file names + 3.1 Add a prefix + 3.2 Add a suffix + 4. Remove a user-defined string from file names + 4.1 Remove a prefix + 4.2 Remove a suffix + 4.3 Remove a string + 5. Replace a user-defined string inside file names + 5.1 Replace a prefix + 5.2 Replace a suffix + 5.3 Replace a string + 6. Strip file names + 7. Exclude certain files + 8. Rename certain files only + + 1. Definition + + The Nomen File Name Modifier script adds, removes or replaces a user- + defined string inside the base name of files. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-filemod.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-filemod.py -d /tmp/pics -a add -p prefix -s "Foobar" + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-filemod.py -d /tmp/pics -a add -p prefix -s "Foobar" \ + --simulate /tmp/report.txt + + 2.5 Prefixes and suffixes + + Typically a file name consists of a base name (prefix) and an + extension (suffix) separated by a separator, for example: + + holiday.jpg + + In this case, "holiday" is the base name, the dot is the separator + and "jpg" is the extension of the file. + + The Nomen File Name Modifier only modifies the base name of file + names. The "--position" argument expects either "any", "prefix" or + "suffix". These positions are only related to the base name, not + to the entire file name. + + For example, if you add the prefix "foo" to the base name it would + result in + + fooholiday.jpg + + and if you add the suffix "foo" (instead of the prefix) the base + name would look as follows: + + holidayfoo.jpg + + As already mentioned above, only the base name (the prefix of the + whole file name) will be modified and the file extension (the + suffix of the whole file name) will be ignored. + + 3. Add a user-defined string to file names + + Before modifying the base name of any files, please see section 2.5 + above. Notice that the given input string is processed case-sensitive. + + 3.1 Add a prefix + + For example, if you have the files + + holiday 1.jpg + holiday 2.jpg + holiday 3.jpg + holiday 4.jpg + + inside the directory "/tmp/pics" and want to add the string + "Summer " (without any quotes) as prefix to the base name of these + files, type: + + ./nomen-filemod.py -d /tmp/pics -a add -p prefix -s "Summer " + + This will rename the base name of the files as follows: + + Summer holiday 1.jpg + Summer holiday 2.jpg + Summer holiday 3.jpg + Summer holiday 4.jpg + + 3.2 Add a suffix + + For example, if you have the files + + Holiday 1.jpg + Holiday 2.jpg + Holiday 3.jpg + Holiday 4.jpg + + inside the directory "/tmp/pics" and want to add the string + " - Foobar" (without any quotes) as suffix to the base name of + these files, type: + + ./nomen-filemod.py -d /tmp/pics -a add -p suffix \ + -s " - Foobar" + + This will rename the base name of the files as follows: + + Holiday 1 - Foobar.jpg + Holiday 2 - Foobar.jpg + Holiday 3 - Foobar.jpg + Holiday 4 - Foobar.jpg + + 4. Remove a user-defined string from file names + + Before modifying the base name of any files, please see section 2.5 + above. Notice that the given input string is processed case-sensitive. + + 4.1 Remove a prefix + + For example, if you have the files + + Summer holiday 1.jpg + Summer holiday 2.jpg + Summer holiday 3.jpg + Summer holiday 4.jpg + + inside the directory "/tmp/pics" and want to remove the prefix + "Summer " (without any quotes) from the base name of these files, + type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p prefix \ + -s "Summer " + + This will rename the base name of the files as follows: + + holiday 1.jpg + holiday 2.jpg + holiday 3.jpg + holiday 4.jpg + + 4.2 Remove a suffix + + For example, if you have the files + + Holiday 1 - Foobar.jpg + Holiday 2 - Foobar.jpg + Holiday 3 - Foobar.jpg + Holiday 4 - Foobar.jpg + + inside the directory "/tmp/pics" and want to remove the suffix + " - Foobar" (without any quotes) from the base name of these + files, type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p suffix \ + -s " - Foobar" + + This will rename the base name of the files as follows: + + Holiday 1.jpg + Holiday 2.jpg + Holiday 3.jpg + Holiday 4.jpg + + 4.3 Remove a string + + This will remove all occurrences of the input string (no matter if + prefix, suffix or somewhere else) in the base name. + + For example, if you have the files + + Foobar Holiday Foobar 1.jpg + Foobar Holiday Foobar 2.jpg + Foobar Holiday Foobar 3.jpg + Foobar Holiday Foobar 4.jpg + + inside the directory "/tmp/pics" and want to remove the string + "Foobar " (without any quotes) from the base name of these files, + type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foobar " + + This will rename the base name of the files as follows: + + Holiday 1.jpg + Holiday 2.jpg + Holiday 3.jpg + Holiday 4.jpg + + 5. Replace a user-defined string inside file names + + Before modifying the base name of any files, please see section 2.5 + above. Notice that the given string (input and replace string) are + processed case-sensitive. + + 5.1 Replace a prefix + + For example, if you have the files + + Winter holiday 1.jpg + Winter holiday 2.jpg + Winter holiday 3.jpg + Winter holiday 4.jpg + + inside the directory "/tmp/pics" and want to replace the prefix + "Winter " with "Summer " (both without any quotes) inside the base + name of these files, type: + + ./nomen-filemod.py -d /tmp/pics -a replace -p prefix \ + -s "Winter " --replace-string "Summer " + + This will rename the base name of the files as follows: + + Summer holiday 1.jpg + Summer holiday 2.jpg + Summer holiday 3.jpg + Summer holiday 4.jpg + + 5.2 Replace a suffix + + For example, if you have the files + + Holiday 1 - Foobar.jpg + Holiday 2 - Foobar.jpg + Holiday 3 - Foobar.jpg + Holiday 4 - Foobar.jpg + + inside the directory "/tmp/pics" and want to replace the suffix + " - Foobar" with " Foo" (both without any quotes) inside the base + name of these files, type: + + ./nomen-filemod.py -d /tmp/pics -a replace -p suffix \ + -s " - Foobar" --replace-string " Foo" + + This will rename the base name of the files as follows: + + Holiday 1 Foo.jpg + Holiday 2 Foo.jpg + Holiday 3 Foo.jpg + Holiday 4 Foo.jpg + + 5.3 Replace a string + + This will replace all occurrences of the input string (no matter + if prefix, suffix or somewhere else) in the base name. + + For example, if you have the files + + Fu - Fubar - Fu 1.jpg + Fu - Fubar - Fu 2.jpg + Fu - Fubar - Fu 3.jpg + Fu - Fubar - Fu 4.jpg + + inside the directory "/tmp/pics" and want to replace the string + "Fu" with "Foo" (both without any quotes) inside the base name of + these files, type: + + ./nomen-filemod.py -d /tmp/pics -a replace -p any -s "Fu" \ + --replace-string "Foo" + + This will rename the base name of the files as follows: + + Foo - Foobar - Foo 1.jpg + Foo - Foobar - Foo 2.jpg + Foo - Foobar - Foo 3.jpg + Foo - Foobar - Foo 4.jpg + + 6. Strip file names + + You can also remove certain leading and trailing characters (such as + whitespaces) from the file name. For example, if you have the file + + Foo Holiday 1 Foo.jpg + + and want to remove the string "Foo" inside the entire file name, you + can do this using + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" + + but this would result in a file name with a leading as well as a + trailing whitespace (" Holiday 1 .jpg"). + + Now, there are multiple ways to remove these whitespaces. The first + way would be explicitly removing the prefix and suffix containing the + whitespace. + + ./nomen-filemod.py -d /tmp/pics -a remove -p prefix -s "Foo " + ./nomen-filemod.py -d /tmp/pics -a remove -p suffix -s " Foo" + + Another way would be removing the string "Foo" first and after that + the leading and trailing whitespaces separately. + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" + ./nomen-filemod.py -d /tmp/pics -a remove -p prefix -s " " + ./nomen-filemod.py -d /tmp/pics -a remove -p suffix -s " " + + The simplest way would be using the included command-line argument: + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" \ + --strip " " + + The "--strip" argument also allows giving multiple characters that + should be stripped. For example, to strip the letter "H" in uppercase + as well as whitespaces, type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" \ + --strip "H " + + However, this will result in "oliday 1.jpg", due to the fact, that the + list of strip characters will not be processed in a certain order. + + 7. Exclude certain files + + See section 4 of the documentation of the Nomen File Renamer script + how to do this. The command-line arguments to exclude files used there + can also be applied to the Nomen File Modifier script. + + 8. Rename certain files only + + See section 5 of the documentation of the Nomen File Renamer script + how to do this. The command-line arguments to rename files only used + there can also be applied to the Nomen File Modifier script. + diff --git a/python2/docs/usage_nomen-fileren.txt b/python2/docs/usage_nomen-fileren.txt new file mode 100644 index 0000000..321188f --- /dev/null +++ b/python2/docs/usage_nomen-fileren.txt @@ -0,0 +1,496 @@ + +USAGE (nomen-fileren.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 2.5 Significant changes (released with version 2.1.0) + 2.5.1 Names of the rename modes + 2.5.2 Way of renaming files + 3. Rename files + 3.1 Using "keep-order" mode + 3.1.1 Order by name (default) + 3.1.2 Order by access time + 3.1.3 Order by creation time + 3.1.4 Order by modification time + 3.2 Using "rename-new" mode + 3.3 Using "fill-gaps" mode + 4. Exclude certain files + 4.1 Exclude certain files using basic strings + 4.2 Exclude certain files using wildcards + 5. Rename certain files only + 5.1 Rename certain files using basic strings + 5.2 Rename certain files using wildcards + 6. Steps between each numeric ID + 6.1 Issue before + 6.2 Solution + + 1. Definition + + The Nomen File Renamer script renames the base name of files within a + directory and (if requested) in all of its sub-directories based on + the name of the directory where the files are stored in and adds a + unique numeric ID. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-fileren.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + --simulate /tmp/report.txt + + 2.5 Significant changes (released with version 2.1.0) + + 2.5.1 Names of the rename modes + + Before Nomen version 2.0.0, there only were two rename modes, + "consecutive" and "fill". + + In version 2.0.0 the "fill" mode has been forked into the two + modes "fillin" and "fillnew". + + With the release of version 2.1.0, all these names have been + replaced with new ones for more clarity. + + +--------------------------+--------------------------+ + | Before | After | + +--------------------------+--------------------------+ + | consecutive | keep-order | + | fillin | fill-gaps | + | fillnew | rename-new | + +--------------------------+--------------------------+ + + 2.5.2 Way of renaming files + + In the past, the files have been renamed with a continuous + number ignoring the file type. + + With the release of version 2.1.0, by default the file types + will not longer be ignored, so every file type has its own + range of numbers: + + +--------------------------+--------------------------+ + | Before | After | + +--------------------------+--------------------------+ + | Picture_0001.bmp | Picture_0001.bmp | + | Picture_0002.bmp | Picture_0002.bmp | + | Picture_0003.jpg | Picture_0001.jpg | + | Picture_0004.jpg | Picture_0002.jpg | + | Picture_0005.png | Picture_0001.png | + | Picture_0006.png | Picture_0002.png | + +--------------------------+--------------------------+ + + However, with the "--ignore-file-ext" command-line argument + this behaviour can be changed, so the files will be renamed + with a continuous number ignoring the file type again. + + 3. Rename files + + For example, if you have four picture files inside the directory + "/tmp/pics/Holidays": + + pic_0001.jpg + pic_0002.jpg + pic_0003.jpg + pic_0004.jpg + + Basically, the Nomen File Renamer script will rename the files inside + that directory based on the directory name where they are stored in + and add a unique numeric ID. For example: + + Holidays 1.jpg + Holidays 2.jpg + Holidays 3.jpg + Holidays 4.jpg + + The script offers some file rename modes. + + 3.1 Using "keep-order" mode + + The "keep-order" mode keeps the chronological order even if files + have been deleted. There are multiple ways this mode can be used. + + 3.1.1 Order by name (default) + + In case no order command-line argument was given, the default + behaviour will be used. For example, if you delete the picture + file "Holidays 2.jpg" and re-run the script with this rename + mode it will do the following: + + Holidays 1.jpg stays (no need to rename) + Holidays 3.jpg will be renamed to "Holidays 2.jpg" + Holidays 4.jpg will be renamed to "Holidays 3.jpg" + + Now, if you add another file to that directory, "foobar.jpg" + for example, and re-run the Nomen File Renamer script with + this rename mode, the new file will be appended to the + existing and already renamed files: + + Holidays 1.jpg stays (no need to rename) + Holidays 2.jpg stays (no need to rename) + Holidays 3.jpg stays (no need to rename) + foobar.jpg will be renamed to "Holidays 4.jpg" + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays + + In case you want to reorganize these pictures, put them into a + new directory called "Summer" among with some other files and + re-run the script with this rename mode, the files will not be + renamed chronologically, but alphabetically. For example: + + Beach 1.jpg will be renamed to "Summer 1.jpg" + Beach 2.jpg will be renamed to "Summer 2.jpg" + Holidays 1.jpg will be renamed to "Summer 3.jpg" + Holidays 2.jpg will be renamed to "Summer 4.jpg" + Holidays 3.jpg will be renamed to "Summer 5.jpg" + Holidays 4.jpg will be renamed to "Summer 6.jpg" + + So, even if the "Beach" pictures are newer than the "Holidays" + pictures the files will be renamed like this. + + You can also keep a certain order, for example, beginning with + the "Holidays" pictures followed by the "Beach" pictures. At + first, put only the "Holidays" pictures into the "Summer" + directory and run the script with this rename mode: + + Holidays 1.jpg will be renamed to "Summer 1.jpg" + Holidays 2.jpg will be renamed to "Summer 2.jpg" + Holidays 3.jpg will be renamed to "Summer 3.jpg" + Holidays 4.jpg will be renamed to "Summer 4.jpg" + + After that, put the "Beach" pictures in the "Summer" directory + and run the script with this mode again: + + Summer 1.jpg stays (no need to rename) + Summer 2.jpg stays (no need to rename) + Summer 3.jpg stays (no need to rename) + Summer 4.jpg stays (no need to rename) + Beach 1.jpg will be renamed to "Summer 5.jpg" + Beach 2.jpg will be renamed to "Summer 6.jpg" + + 3.1.2 Order by access time + + Please read section 3.1.1 above first before continuing with + this one. + + You can also rename and order the files by the time they have + been accessed in ascending order. So, the file with the oldest + time will be the first one to be renamed. + + However, the access time is not being determined by Nomen, it + is the timestamp provided by the metadata of the file. + + Notice that this will irreversibly change the order of the + files. + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + -o accessed + + 3.1.3 Order by creation time + + Please read section 3.1.1 above first before continuing with + this one. + + You can also rename and order the files by the time they have + been created in ascending order. So, the file with the oldest + time will be the first one to be renamed. + + However, the creation time is not being determined by Nomen, + it is the timestamp provided by the metadata of the file. + + Notice that this will irreversibly change the order of the + files. + + According to the official Python documentation (see "Common + pathname manipulations"), the creation time information + depends on the operation system. + + On some systems (like Unix) it is the time of the last change + of the metadata, and, on others (like Windows), is the + creation time of the file. + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + -o created + + 3.1.4 Order by modification time + + Please read section 3.1.1 above first before continuing with + this one. + + You can also rename and order the files by the time they have + been modified in ascending order. So, the file with the oldest + time will be the first one to be renamed. + + However, the modification time is not being determined by + Nomen, it is the timestamp provided by the metadata of the + file. + + Notice that this will irreversibly change the order of the + files. + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + -o modified + + 3.2 Using "rename-new" mode + + The "rename-new" mode does NOT keep the chronological order if + files have been deleted. It simply fills the numeration gaps with + new files or files that have been renamed manually. + + For example, if you delete the picture file "Holidays 2.jpg" and + re-run the Nomen File Renamer script with this rename mode, no + file names will be changed. + + Holidays 1.jpg stays (no need to rename) + Holidays 3.jpg stays (no need to rename) + Holidays 4.jpg stays (no need to rename) + + If you then add another file to that directory, e. g. "foobar.jpg" + and re-run the script this rename mode again, it will simply fill + the gap "Holiday 2.jpg" has left using the new file: + + Holidays 1.jpg stays (no need to rename) + Holidays 3.jpg stays (no need to rename) + Holidays 4.jpg stays (no need to rename) + foobar.jpg will be renamed to "Holidays 2.jpg" + + To use this rename mode, type: + + ./nomen-fileren.py -m rename-new -d /tmp/pics/Holidays + + 3.3 Using "fill-gaps" mode + + The "fill-gaps" mode works like the "rename-new" mode (for details + see section 3.2) with the difference, that it fills all numeration + gaps using new as well as the existing files with the greatest + numbers. For example: + + Holidays 1.jpg stays (no need to rename) + Holidays 4.jpg stays (no need to rename) + Holidays 5.jpg will be renamed to "Holidays 3.jpg" + foobar.jpg will be renamed to "Holidays 2.jpg" + + To use this rename mode, type: + + ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays + + 4. Exclude certain files + + 4.1 Exclude certain files using basic strings + + For example, to exclude all files whose file name (base name or + extension) contains the string "foo", type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "foo" + + By default, the given pattern is being processed case-insensitive. + That means, the string given above would match "foo" as well as + e. g. "FOO", "Foo", "FoO" and so on. + + The pattern can also consist of multiple strings (separated with + semicolons): + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "foo2;foo4" + + 4.2 Exclude certain files using wildcards + + The pattern also supports asterisk wildcards. So, if you want to + exclude all files whose file name (base name or extension) + contains a string that starts with "foo" and ends with "bar" + (e. g. "foobar", "foo-bar" or "foo bar"), type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "foo*bar" + + The pattern can also consist of multiple strings (separated with + semicolons) with wildcards: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "f*o;b*r" + + 5. Rename certain files only + + 5.1 Rename certain files using basic strings + + So, for example, to only rename the files whose file name (base + name or extension) contains the string "foo", type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "foo" + + By default, the given pattern is being processed case-insensitive. + That means, the string given above would match "foo" as well as + e. g. "FOO", "Foo", "FoO" and so on. + + The pattern can also consist of multiple strings (separated with + semicolons): + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "foo2;foo4" + + 5.2 Rename certain files using wildcards + + The pattern also supports asterisk wildcards. So, if you want to + only rename the files whose file name (base name or extension) + contains a string that starts with "foo" and ends with "bar" + (e. g. "foobar", "foo-bar" or "foo bar"), type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "foo*bar" + + The pattern can also consist of multiple strings (separated with + semicolons) with wildcards: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "f*o;b*r" + + 6. Steps between each numeric ID + + Since version 2.3.0, it is also possible to set an additional user- + defined step value, which can be combined with all rename modes + mentioned in section 3 above. + + Especially when using the the "keep-order" mode, adding new files to + existing ones between two numeric IDs was quite inconvenient. Below + you can find an example issue and its solution. + + 6.1 Issue before + + There is a directory containing already renamed files and a new + one that should be between others like this + + Holidays 1.jpg + Holidays 2.jpg + Holidays 3.jpg + pic_0001.jpg should actually be "Holidays 2.jpg" + + and running + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays + + would rename "pic_0001.jpg" to "Holidays 4.jpg", which is not + what we want. + + One way to solve that is: + + 1. Create a new temporary sub-directory with a different name, + for example "Temp". + + 2. Move all files whose names are starting with "Holdays" into + that directory. + + 3. Then, run the Nomen File Renamer script again. The file + "pic_0001.jpg" file will then renamed to "Holidays 2.jpg". + + 4. In case that the target directory has not been processed + recursively before, run the script once again with the path + of the sub-directory as target directory, so the term + "Holidays" inside the file name will be replaced by "Temp". + + 5. Move all files from the temporary directory back into the + directory where the "Holidays" files are. + + 6. Delete the temporary directory. + + 7. Now, run the Nomen File Renamer script a third time with + the path of the directory that contains the "Holidays" + files as the target directory again. + + 6.2 Solution + + Now, before adding new files that should be between others, run + the Nomen File Renamer script (before adding new files) with a + user-defined step value, for example 10. + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + --step 10 + + This will rename the files as follows: + + Holidays 10.jpg + Holidays 20.jpg + Holidays 30.jpg + + Then add the "pic_0001.jpg" file to that directiry and rename it + like "Holidays 11.jpg" (or with any number between 10 and 20). + + Holidays 10.jpg stays (no need to rename) + Holidays 11.jpg will be renamed to "Holidays 20.jpg" + Holidays 20.jpg will be renamed to "Holidays 30.jpg" + Holidays 30.jpg will be renamed to "Holidays 3.jpg" + + Now, run the Nomen File Renamer script once again + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + --step 10 + + to apply the changes: + + Holidays 10.jpg stayed (no need to rename) + Holidays 20.jpg was "Holidays 11.jpg" before + Holidays 30.jpg was "Holidays 20.jpg" before + Holidays 40.jpg was "Holidays 30.jpg" before + diff --git a/python2/license.txt b/python2/license.txt new file mode 100644 index 0000000..5c861af --- /dev/null +++ b/python2/license.txt @@ -0,0 +1,27 @@ + +LICENSE (Nomen) + + Nomen - Multi-purpose rename tool + Copyright (C) 2018 by Ralf Kilian + + Distributed under the MIT License: + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/python2/nomen-dirspace.py b/python2/nomen-dirspace.py new file mode 100755 index 0000000..c8f6fac --- /dev/null +++ b/python2/nomen-dirspace.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Directory name space modifier script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print "%s: error: %s" % (os.path.basename(sys.argv[0]), e) + sys.exit(1) + + p.set_description("Modify a directory name by removing leading, " \ + "trailing and duplicate spaces or by inserting and " \ + "removing spaces next to punctuation characters.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + + # Optional arguments + p.add_switch("-b", "--brackets", "insert and remove spaces next to " \ + "brackets", "brackets", True, False) + p.add_avalue(None, "--exclude", "pattern to exclude certain files " \ + "(case-insensitive, multiple patterns separated via " \ + "semicolon)", "exclude_pattern", None, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--hyphens", "insert spaces next to hyphens", \ + "hyphens", True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-l", "--remove-leading", "remove leading spaces", + "remove_leading", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_switch("-p", "--punctuation", "insert and remove spaces next to " \ + "punctuation characters", "punctuation", True, False) + p.add_switch("-s", "--remove-duplicate", "remove duplicate spaces", + "remove_duplicate", True, False) + p.add_switch("-t", "--remove-trailing", "remove trailing spaces", + "remove_trailing", True, False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print fileren.get_version() + sys.exit(0) + + try: + args = p.parse_args() + if not args.punctuation and not args.remove_duplicate and \ + not args.remove_leading and not args.remove_trailing: + p.error("Nothing to do (no optional arguments were given).") + + common.dir_space_modifier(args.directory, args.remove_duplicate, + args.remove_leading, args.remove_trailing, + args.brackets, args.hyphens, + args.punctuation, args.ignore_symlinks, + args.recursive, args.exclude_pattern) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python2/nomen-extcase.py b/python2/nomen-extcase.py new file mode 100755 index 0000000..2631613 --- /dev/null +++ b/python2/nomen-extcase.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Extension case converter script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import extren + + try: + p = clap.Parser() + except Exception as e: + print "%s: error: %s" % (os.path.basename(sys.argv[0]), e) + sys.exit(1) + + p.set_description("Convert the case of the extension of all files " \ + "inside a directory and (if requested) in all of its " \ + "sub-directories.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_predef("-c", "--case", "target extension case", "case", + ["lower", "title", "upper"], True) + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-m", "--conflict-mode", "conflict mode (in case of " \ + "duplicate file names)", "conflict_mode", ["rename", "skip"], + True) + + # Optional arguments + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print extren.get_version() + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + extren.convert_case(args.directory, args.case, args.conflict_mode, + args.recursive, args.report_file, + args.ignore_symlinks) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python2/nomen-extren.py b/python2/nomen-extren.py new file mode 100755 index 0000000..cc52243 --- /dev/null +++ b/python2/nomen-extren.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Extension renamer script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import extren + + try: + p = clap.Parser() + except Exception as e: + print "%s: error: %s" % (os.path.basename(sys.argv[0]), e) + sys.exit(1) + + p.set_description("Rename (and adjust) differently spelled file " \ + "extensions of the same file type file within a " \ + "directory and (if requested) in all of its sub-" \ + "directories.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_avalue("-e", "--extension", "extension to rename (case-" \ + "sensitive, multiple extensions separated via semicolon)", + "extension", None, True) + p.add_predef("-m", "--conflict-mode", "conflict mode (in case of " \ + "duplicate file names)", "conflict_mode", ["rename", "skip"], + True) + p.add_avalue("-t", "--target-extension", "target extension (case-" \ + "sensitive)", "extension_target", None, True) + + # Optional arguments + p.add_switch("-c", "--case-sensitive", "do not ignore the case of the " \ + "given extension list", "case", False, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print extren.get_version() + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + extren.rename_extensions(args.directory, args.conflict_mode, + args.extension, args.extension_target, + args.recursive, args.case, args.report_file, + args.ignore_symlinks) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python2/nomen-filecase.py b/python2/nomen-filecase.py new file mode 100755 index 0000000..3ff0c72 --- /dev/null +++ b/python2/nomen-filecase.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File name case converter script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print "%s: error: %s" % (os.path.basename(sys.argv[0]), e) + sys.exit(1) + + p.set_description("Convert the case of the base name of all files " \ + "inside a directory and (if requested) in all of its " \ + "sub-directories.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_predef("-c", "--case", "target case of the base name", "case", + ["lower", "title", "upper", "config"], True) + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-m", "--conflict-mode", "conflict mode (in case of " \ + "duplicate file names)", "conflict_mode", ["rename", "skip"], + True) + + # Optional arguments + p.add_avalue(None, "--cfg-lower", "path to the config file for strings " \ + "which should always be lowercase inside the file " \ + "name", "cfg_lower", None, False) + p.add_avalue(None, "--cfg-mixed", "path to the config file for strings " \ + "which should always be mixed case inside the " \ + "file name", "cfg_mixed", None, False) + p.add_avalue(None, "--cfg-title", "path to the config file for strings " \ + "which should always be title case inside the " \ + "file name", "cfg_title", None, False) + p.add_avalue(None, "--cfg-upper", "path to the config file for strings " \ + "which should always be uppercase inside the file " \ + "name", "cfg_upper", None, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print fileren.get_version() + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + fileren.convert_case(args.directory, args.case, args.conflict_mode, + args.recursive, args.cfg_lower, args.cfg_mixed, + args.cfg_title, args.cfg_upper, args.report_file, + args.ignore_symlinks) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python2/nomen-filemod.py b/python2/nomen-filemod.py new file mode 100755 index 0000000..e69a824 --- /dev/null +++ b/python2/nomen-filemod.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File name modifier script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print "%s: error: %s" % (os.path.basename(sys.argv[0]), e) + sys.exit(1) + + p.set_description("Modify the base name of files by adding, removing " \ + "or replacing a user-defined string.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_predef("-a", "--action", "action to perform", "action", + ["add", "remove", "replace"], True) + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-p", "--position", "position where to perform the action", + "position", ["any", "prefix", "suffix"], True) + p.add_avalue("-s", "--string", "input string to perform the action " \ + "with (case-sensitive)", "input_string", None, True) + + # Optional arguments + p.add_switch("-c", "--case-sensitive", "do not ignore the case of the " \ + "given exclude or explicit pattern", "case", False, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_avalue(None, "--exclude", "pattern to exclude certain files " \ + "(case-insensitive, multiple patterns separated via " \ + "semicolon)", "exclude_pattern", None, False) + p.add_avalue(None, "--explicit", "explicit pattern to only process " \ + "certain files (case-insensitive, multiple patterns " \ + "separated via semicolon)", "explicit_pattern", None, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_switch(None, "--regex", "use regex syntax for the exclude or " \ + "explicit pattern instead of just asterisk wildcards and " \ + "semicolon separators (for details see the section " \ + "'Regular expression operations' inside the official " \ + "Python documentation)", "regex_syntax", True, False) + p.add_avalue(None, "--replace-string", "string to replace the input" \ + "string with (when using the action 'replace')", + "replace_string", None, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_avalue(None, "--strip", "remove certain leading and trailing " \ + "characters from the base name", "strip_chars", None, False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print fileren.get_version() + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + if args.exclude_pattern and args.explicit_pattern: + p.error("The exclude and the explicit pattern argument cannot be " \ + "given at the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + if args.exclude_pattern: + pattern = args.exclude_pattern + exclude = True + elif args.explicit_pattern: + exclude = False + pattern = args.explicit_pattern + else: + exclude = None + pattern = None + + fileren.modify_names(args.directory, args.action, args.position, + args.input_string, args.replace_string, + args.recursive, exclude, pattern, args.case, + args.regex_syntax, args.report_file, + args.ignore_symlinks, args.strip_chars) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python2/nomen-fileren.py b/python2/nomen-fileren.py new file mode 100755 index 0000000..e2014d3 --- /dev/null +++ b/python2/nomen-fileren.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File renamer script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print "%s: error: %s" % (os.path.basename(sys.argv[0]), e) + sys.exit(1) + + p.set_description("Rename the base name of files within a directory " \ + "and (if requested) in all of its sub-directories " \ + "based on the name of the directory where the files " \ + "are stored in and add a unique numeric ID.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-m", "--rename-mode", "rename mode to use", "rename_mode", + ["fill-gaps", "increase", "keep-order", "rename-new"], True) + + # Optional arguments + p.add_switch("-c", "--case-sensitive", "do not ignore the case of the " \ + "given exclude or explicit pattern", "case", False, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_avalue(None, "--custom-name", "custom file name (instead of the " \ + "directory name where the files are stored in)", + "custom_name", None, False) + p.add_avalue(None, "--exclude", "pattern to exclude certain files " \ + "(case-insensitive, multiple patterns separated via " \ + "semicolon)", "exclude_pattern", None, False) + p.add_avalue(None, "--explicit", "explicit pattern to only process " \ + "certain files (case-insensitive, multiple patterns " \ + "separated via semicolon)", "explicit_pattern", None, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-file-ext", "ignore file extensions when " \ + "numerating files", "ignore_file_ext", True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_predef("-o", "--order-by", "order files by last accessed, " \ + "created or modified date", "order_by", ["accessed", + "created", "modified"], False) + p.add_avalue("-p", "--padding", "set a user-defined numeric padding " \ + "(if no user-defined padding value is given, it will be " \ + "set automatically based on the amount of files per " \ + "directory)", "padding", 0, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_switch(None, "--regex", "use regex syntax for the exclude or " \ + "explicit pattern instead of just asterisk wildcards and " \ + "semicolon separators (for details see the section " \ + "'Regular expression operations' inside the official " \ + "Python documentation)", "regex_syntax", True, False) + p.add_avalue("-s", "--separator", "use a user-defined character or " \ + "string as a separator between the directory name and the " \ + "unique numeric ID", "separator", " ", False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_avalue(None, "--step", "steps between each numeric ID", "step", 1, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print fileren.get_version() + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + if args.exclude_pattern and args.explicit_pattern: + p.error("The exclude and the explicit pattern argument cannot be " \ + "given at the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + if args.exclude_pattern: + pattern = args.exclude_pattern + exclude = True + elif args.explicit_pattern: + exclude = False + pattern = args.explicit_pattern + else: + exclude = None + pattern = None + + fileren.rename_files(args.directory, args.rename_mode, args.separator, + args.recursive, args.padding, exclude, pattern, + args.case, args.regex_syntax, args.report_file, + args.ignore_symlinks, args.ignore_file_ext, + args.custom_name, args.step, args.order_by) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python2/readme.txt b/python2/readme.txt new file mode 100644 index 0000000..bbf06d3 --- /dev/null +++ b/python2/readme.txt @@ -0,0 +1,64 @@ + +README (Nomen) + + Project + + Nomen + Version 2.3.5 (based on Python framework 2.x) + Copyright (C) 2018 by Ralf Kilian + + Website: http://www.urbanware.org + GitHub: https://github.com/urbanware-org/nomen + + Definition + + The Nomen project is a multi-purpose rename tool to consistently + rename the base name as well as the extension of files in a variety of + ways and also to remove unnecessary whitespaces from directory names. + + License + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Requirements + + This version of Nomen was built for the Python 2.x framework. If you + need a version that works with Python 3.x, you can also find one on + the website of the project mentioned above. + + In order to use the project, Python 2.7 or higher is recommended, but + it may also work with earlier versions. + + Usage + + For fundamental documentation as well as some usage examples for each + component of the project, you may have a look at the text files inside + the included 'docs' sub-directory. + + Legal information + + The project name is completely fictitious. Any correspondences with + existing websites, applications, companies and/or other projects are + purely coincidental. + + All trademarks belong to their respective owners. + + Errors and omissions excepted. + diff --git a/python3/cfg/sample_lower.cfg b/python3/cfg/sample_lower.cfg new file mode 100644 index 0000000..1e79765 --- /dev/null +++ b/python3/cfg/sample_lower.cfg @@ -0,0 +1,67 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be # +# LOWERCASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "foo" shall always be lowercase in file names. +# +# FOOBAR.TXT => file will NOT be renamed (because "FOO" is not a +# separate word) +# FOO BAR.TXT => file will be renamed to "foo BAR.txt" +# +foo + +# Example 2: +# +# The string "foo" shall always be lowercase in file names. +# +# FOOBAR.TXT => file will be renamed to "fooBAR.txt" +# FOO BAR.TXT => file will be renamed to "foo BAR.txt" +# +$(foo) + +# Example 3: +# +# The string "bar" shall always be lowercase in file names. +# +# FOOBAR.TXT => file will be renamed to "FOObar.txt" +# FOO BAR.TXT => file will be renamed to "FOO bar.txt" +# +$(bar) + +# Example 4: +# +# When converting the case of the files +# +# FOOBAR'S ADVENTURE.TXT +# MARTIN O'SULLIVAN.TXT +# +# using title case, the file will be renamed as follows: +# +# Foobar'S Adventure.TXT +# Martin O'Sullivan.TXT +# +# So, if you want to convert the "'S" to lowercase, you could use +# +# $('s) +# +# but this will also rename "O'Sullivan" to "O'sullivan". To avoid this you +# may use the following statement. It only converts "'S" to lowercase if a +# whitespace follows. +# +$('s ) + +# EOF + diff --git a/python3/cfg/sample_mixed.cfg b/python3/cfg/sample_mixed.cfg new file mode 100644 index 0000000..7a42e96 --- /dev/null +++ b/python3/cfg/sample_mixed.cfg @@ -0,0 +1,67 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be MIXED # +# CASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "FoO" shall always be mixed case in file names. +# +# FOOBAR.TXT => file will NOT be renamed (because "FOO" is not a +# separate word) +# FOO BAR.TXT => file will be renamed to "FoO BAR.txt" +# +FoO + +# Example 2: +# +# The string "FoO" shall always be mixed case in file names. +# +# FOOBAR.TXT => file will be renamed to "FoOBAR.txt" +# FOO BAR.TXT => file will be renamed to "FoO BAR.txt" +# +$(FoO) + +# Example 3: +# +# The string "bAr" shall always be mixed case in file names. +# +# FOOBAR.TXT => file will be renamed to "FOObAr.txt" +# FOO BAR.TXT => file will be renamed to "FOO bAr.txt" +# +$(bAr) + +# Example 4: +# +# When converting the case of the files +# +# FOOBAR'S ADVENTURE.TXT +# MARTIN O'SULLIVAN.TXT +# +# using title case, the file will be renamed as follows: +# +# Foobar'S Adventure.TXT +# Martin O'Sullivan.TXT +# +# So, if you want to convert the "'S" to lowercase, you could use +# +# $('s) +# +# but this will also rename "O'Sullivan" to "O'sullivan". To avoid this you +# may use the following statement. It only converts "'S" to lowercase if a +# whitespace follows. +# +$('s ) + +# EOF + diff --git a/python3/cfg/sample_title.cfg b/python3/cfg/sample_title.cfg new file mode 100644 index 0000000..2eecc26 --- /dev/null +++ b/python3/cfg/sample_title.cfg @@ -0,0 +1,45 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be TITLE # +# CASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "Foo" shall always be title case in file names. +# +# FOOBAR.TXT => file will NOT be renamed (because "FOO" is not a +# separate word) +# FOO BAR.TXT => file will be renamed to "Foo BAR.txt" +# +Foo + +# Example 2: +# +# The string "Foo" shall always be title case in file names. +# +# FOOBAR.TXT => file will be renamed to "FooBAR.txt" +# FOO BAR.TXT => file will be renamed to "Foo BAR.txt" +# +$(Foo) + +# Example 3: +# +# The string "Foo" shall always be title case in file names. +# +# FOOBAR.TXT => file will be renamed to "FOOBar.txt" +# FOO BAR.TXT => file will be renamed to "FOO Bar.txt" +# +$(Bar) + +# EOF + diff --git a/python3/cfg/sample_upper.cfg b/python3/cfg/sample_upper.cfg new file mode 100644 index 0000000..6eb268a --- /dev/null +++ b/python3/cfg/sample_upper.cfg @@ -0,0 +1,45 @@ +############################################################################## +# # +# Nomen sample configuration file for strings that should ALWAYS be # +# UPPERCASE when converting the case of file names. # +# # +# This will NOT convert the case of the file extension. For renaming the # +# extension, you may use the Nomen Extention Case Converter script. # +# # +# One string per line, empty and commented out lines (like this header) # +# will be ignored. # +# # +# Please read the documentation before using any component of Nomen. # +# # +############################################################################## + +# Example 1: +# +# The word "FOO" shall always be uppercase in file names. +# +# foobar.txt => file will NOT be renamed (because "foo" is not a +# separate word) +# foo bar.txt => file will be renamed to "FOO bar.txt" +# +FOO + +# Example 2: +# +# The string "FOO" shall always be uppercase in file names. +# +# foobar.txt => file will be renamed to "FOObar.txt" +# foo bar.txt => file will be renamed to "FOO bar.txt" +# +$(FOO) + +# Example 3: +# +# The string "BAR" shall always be uppercase in file names. +# +# foobar.txt => file will be renamed to "fooBAR.txt" +# foo bar.txt => file will be renamed to "foo BAR.txt" +# +$(BAR) + +# EOF + diff --git a/python3/changelog.txt b/python3/changelog.txt new file mode 100644 index 0000000..b2a03db --- /dev/null +++ b/python3/changelog.txt @@ -0,0 +1,195 @@ + +CHANGELOG (Nomen) + + Version 2.3.5 (2018-04-14) + + + Added new versions of the Clap and PaVal core modules (replaced the + existing ones). + + * Revised (refurbished) all components of the project in general + (negligible changes). + + Version 2.3.4 (2016-07-12) + + + Added a simple random string generator method to the Nomen Common + core module. + + * Revised the confirmation prompt inside the Nomen Common core module + (now uses random strings instead of a fixed one). + + # Fixed the type mismatch bug inside the Nomen File Renamer core + module (occurred in case a file had the correct prefix but without + a trailing number). + + Version 2.3.3 (2016-06-20) + + + Added a new feature to the Nomen File Renamer component that allows + to order the files by their access, creation and modification time. + + # Fixed the maximum recursion depth exceeded bug inside the Nomen + Common core module (occurred under certain circumstances, only) + + Version 2.3.2 (2016-05-19) + + * Revised some code inside the Nomen Directory Name Space Modifier + script changes). + + # Fixed the path referenced before assignment bug inside the Nomen + Common core module (occurred under certain circumstances, only). + + Version 2.3.1 (2016-05-17) + + + Added a basic exclude feature to the Nomen Directory Name Space + Modifier script (case-insensitive, without regex syntax). + + # Fixed the character replacement bug inside a method of the Nomen + Common core module (used by Nomen Directory Name Space Modifier) + which occurred when the recurisve parameter was set. + + Version 2.3.0 (2016-04-16) + + + Added a new feature to the Nomen File Renamer component that allows + user-defined steps between each numeric ID. + + * Revised some code inside the Nomen Common core module (negligible + changes). + * Revised some parameter validation code in various core modules. + + Version 2.2.6 (2015-06-05) + + + Added some additional command-line arguments to the Nomen Directory + Name Space Modifier script to insert and remove spaces around + brackets and hyphens. + + * Revised some code inside the Nomen Common core module (negligible + changes). + + # Fixed the character replacement bug inside a method of the Nomen + Common core module (used by Nomen Directory Name Space Modifier). + + Version 2.2.5 (2015-01-24) + + + Added an optional feature to the Nomen Directory Name Space Modifier + (former "Nomen Directory Name Space Remover") to insert and remove + spaces next to punctuation characters. + + * Revised the confirmation prompt inside the Nomen Common core module. + + Version 2.2.4 (2015-01-03) + + * Revised the code for handling regular expressions inside the core + modules. + + - Removed unnecessary imports and unused variables from the core + modules. + + Version 2.2.3 (2014-08-25) + + + Added the Nomen Directory Name Space Remover component. + + * Revised (enhanced) the Nomen Common core module. + + Version 2.2.2 (2014-06-28) + + + Added a feature that allows omitting the config file path if the + file is located in the corresponding config sub-directory. + + * Revised the method to parse the case config files inside the Nomen + Static Case core module (added default values). + + Version 2.2.1 (2014-05-22) + + + Added an optional command-line argument to the Nomen File Name + Modifier script to remove certain leading and trailing characters + from the processed base name. + + # Fixed the file skip bug inside the Nomen File Name Modifier module + when processing files whose base name contains dots. + + Version 2.2.0 (2014-05-16) + + + Added the new Nomen File Name Modifier component. + + * Revised (reduced) some code inside the Nomen Static Case core + module. + * Revised (renamed) some command-line arguments inside all scripts + (for more clarity). + + - Removed some unnecessary code from the Nomen File Renamer core + module. + + Version 2.1.6 (2014-05-14) + + + Added a method to the Nomen Common core module that returns invalid + file name characters. + + * Revised (reduced) some code inside the Nomen File Renamer as the + well as Nomen Extension Renamer core module (negligible changes). + * Revised the list of invalid file name characters. + + Version 2.1.5 (2014-04-22) + + * Revised (reduced) the code inside the Nomen Common core module that + determines if a file with the new name already exists. + * Revised (reduced) the code inside the Nomen Common core module that + determines the case sensitivity of the file system. + + # Fixed the rename bug ("no such file or directory") inside the Nomen + Common core module when renaming files using the "rename-new" or + "fill-gaps" mode (only occurred on case-insensitive file systems). + + Version 2.1.4 (2014-04-19) + + # Fixed the attribute error inside the code of the Nomen Common core + module that determines if a file with the new name already exists. + # Fixed the file system case sensitivity bug inside Nomen Common core + module (which did not determine the case sensitivity properly). + # Fixed the list bug inside the Nomen File Renamer core module (which + did not append all skipped files to the list when using "fill-gaps" + mode). + + Version 2.1.3 (2014-04-12) + + + Added an optional command-line argument to the Nomen File Renamer + script to set a custom file name. + + * Revised the message text of some exceptions (for more clarity). + + Version 2.1.2 (2014-03-28) + + * Revised (reduced) the code inside the Nomen Common core module to + check if a file already has been processed. + * Revised the code inside the Nomen Common core module to check if the + file system is case-sensitive. + + Version 2.1.1 (2014-03-22) + + + Added an optional command-line argument to the Nomen File Renamer + script to ignore the file extension when numerating files. + + Added the estimated time to the simulation report file. + + * Revised (reduced) some code inside the Nomen Common core module. + + # Fixed the overwrite bug that occurred due to different cases of the + file extensions when renaming files via the Nomen File Renamer on + case-insensitive file systems (such as FAT32 and NTFS). + + Version 2.1.0 (2014-03-19) + + * Revised the "fill" rename methods (merged them to one method and + removed unused variables) inside the Nomen File Renamer core module. + * Revised (changed) the names of the rename modes inside the Nomen + File Renamer component (for more clarity). + * Revised the way of renaming files (numeration is now dependent of + the file extension) inside the Nomen File Renamer core module. + + # Fixed the attribute error inside the Nomen Common core module when + reading out the major version of the Python framework using Python + version 2.6 or below. + # Fixed the empty list bug inside the Nomen File Renamer core module + (which terminated the whole file rename process, if occurred). + + Version 2.0.0 (2014-03-18) + + * First official release of this major version. + diff --git a/python3/core/__init__.py b/python3/core/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/python3/core/clap.py b/python3/core/clap.py new file mode 100755 index 0000000..e502975 --- /dev/null +++ b/python3/core/clap.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Clap - Command-line argument parser module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/clap +# ============================================================================ + +__version__ = "1.1.10" + + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + + +class Parser(object): + """ + Project independent command-line argument parser class. + """ + __arg_grp_opt = None + __arg_grp_req = None + __arg_parser = None + __is_argparser = False + __conflict_handler = "resolve" # used by OptionParser, only + + def __init__(self): + try: + from argparse import ArgumentParser + self.__arg_parser = ArgumentParser(add_help=False) + self.__arg_grp_req = \ + self.__arg_parser.add_argument_group("required arguments") + self.__arg_grp_opt = \ + self.__arg_parser.add_argument_group("optional arguments") + self.__is_argparser = True + return + except ImportError: + # Ignore the exception and proceed with the fallback + pass + + try: + from optparse import OptionParser + self.__arg_parser = \ + OptionParser(conflict_handler=self.__conflict_handler) + self.__arg_grp_req = \ + self.__arg_parser.add_option_group("Required arguments") + self.__arg_grp_opt = \ + self.__arg_parser.add_option_group("Optional arguments") + return + except ImportError: + # This should never happen + raise ImportError("Failed to initialize an argument parser.") + + def add_avalue(self, arg_short, arg_long, arg_help, arg_dest, arg_default, + arg_required): + """ + Add an argument that expects a single user-defined value. + """ + if arg_required: + obj = self.__arg_grp_req + else: + obj = self.__arg_grp_opt + + if arg_default is not None: + # Enclose the value with quotes in case it is not an integer + quotes = "'" + try: + arg_default = int(arg_default) + quotes = "" + except ValueError: + pass + + if arg_help.strip().endswith(")"): + arg_help = arg_help.rstrip(")") + arg_help += ", default is %s%s%s)" % \ + (quotes, str(arg_default), quotes) + else: + arg_help += " (default is %s%s%s)" % \ + (quotes, str(arg_default), quotes) + + if self.__is_argparser: + if arg_short is None: + obj.add_argument(arg_long, help=arg_help, dest=arg_dest, + default=arg_default, required=arg_required) + else: + obj.add_argument(arg_short, arg_long, help=arg_help, + dest=arg_dest, default=arg_default, + required=arg_required) + else: + if arg_short is None: + obj.add_option(arg_long, help=arg_help, dest=arg_dest, + default=arg_default) + else: + obj.add_option(arg_short, arg_long, help=arg_help, + dest=arg_dest, default=arg_default) + + def add_predef(self, arg_short, arg_long, arg_help, arg_dest, arg_choices, + arg_required): + """ + Add an argument that expects a certain predefined value. + """ + if arg_required: + obj = self.__arg_grp_req + else: + obj = self.__arg_grp_opt + + if self.__is_argparser: + if arg_short is None: + obj.add_argument(arg_long, help=arg_help, dest=arg_dest, + choices=arg_choices, required=arg_required) + else: + obj.add_argument(arg_short, arg_long, help=arg_help, + dest=arg_dest, choices=arg_choices, + required=arg_required) + else: + if arg_short is None: + obj.add_option(arg_long, help=arg_help, dest=arg_dest, + choices=arg_choices) + else: + # The OptionParser does not print the values to choose from, + # so these have to be added manually to the description of + # the argument first + arg_help += " (choose from " + for item in arg_choices: + arg_help += "'%s', " % item + arg_help = arg_help.rstrip(", ") + ")" + + obj.add_option(arg_short, arg_long, help=arg_help, + dest=arg_dest) + + def add_switch(self, arg_short, arg_long, arg_help, arg_dest, arg_store, + arg_required): + """ + Add an argument that does not expect anything, but returns a + boolean value. + """ + if arg_required: + obj = self.__arg_grp_req + else: + obj = self.__arg_grp_opt + + if arg_store: + arg_store = "store_true" + else: + arg_store = "store_false" + + if self.__is_argparser: + if arg_short is None: + obj.add_argument(arg_long, help=arg_help, dest=arg_dest, + action=arg_store, required=arg_required) + else: + obj.add_argument(arg_short, arg_long, help=arg_help, + dest=arg_dest, action=arg_store, + required=arg_required) + else: + if arg_short is None: + obj.add_option(arg_long, help=arg_help, dest=arg_dest, + action=arg_store) + else: + obj.add_option(arg_short, arg_long, help=arg_help, + dest=arg_dest, action=arg_store) + + def dependency(self, arg_name, arg_value, dependency): + """ + Check the dependency of a command-line argument. + """ + if dependency is not None: + if arg_value is None or str(arg_value) == "": + raise Exception("The '%s' argument depends on %s'." % + (arg_name, dependency)) + + def error(self, obj): + """ + Raise an error and cause the argument parser to print the error + message. + """ + if type(obj) == str: + obj = obj.strip() + + self.__arg_parser.error(obj) + + def parse_args(self): + """ + Parse and return the command-line arguments. + """ + if self.__is_argparser: + args = self.__arg_parser.parse_args() + else: + (args, values) = self.__arg_parser.parse_args() + return args + + def print_help(self): + """ + Print the usage, description, argument details and epilog. + """ + self.__arg_parser.print_help() + + def set_description(self, string): + """ + Set the description text. + """ + self.__arg_parser.description = string.strip() + + def set_epilog(self, string): + """ + Set the epilog text. + """ + self.__arg_parser.epilog = string.strip() + +# EOF diff --git a/python3/core/common.py b/python3/core/common.py new file mode 100755 index 0000000..430fb5e --- /dev/null +++ b/python3/core/common.py @@ -0,0 +1,555 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Common core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +import os +from . import paval as pv +import random +import re +import sys +import tempfile + +from datetime import datetime as dt + +def compile_regex(string, ignore_case=True, regex_syntax=False): + """ + Compile a regular expression from the given pattern string. + """ + pv.string(string, "regular expression", True, None) + if regex_syntax: + pattern = ".*" + string + ".*" + else: + spec_chars = [ "\\", ".", "^", "$", "+", "?", "{", "}", "[", "]", + "|", "(", ")" ] + for char in spec_chars: + string = string.replace(char, "\\" + char) + string = string.strip("*").strip(";") + while ("*" * 2) in string: + string = string.replace(("*" * 2), "*") + while (";" * 2) in string: + string = string.replace((";" * 2), ";") + + list_string = string.split(";") + if len(list_string) > 0: + pattern = "" + for crit in list_string: + if not crit == "": + pattern += "(.*" + crit.replace("*", ".*") + ".*)|" + pattern = pattern.rstrip("|") + if pattern == "": + raise Exception("The given string does not make sense this " \ + "way.") + + if ignore_case: + regex = re.compile(pattern, re.IGNORECASE) + else: + regex = re.compile(pattern) + + return regex + +def confirm_notice(): + """ + Display a notice which must be confirmed by the user to proceed. + """ + string = random_string(6, True, True, True, True) + proceed = False + notice_text = """ o o o 88 + 8 8 88 + 8 8 .oPYo. oPYo. odYo. o8 odYo. .oPYo. 88 + 8 db 8 .oooo8 8 '' 8' '8 8 8' '8 8 8 88 + 'b.PY.d' 8 8 8 8 8 8 8 8 8 8 + '8 8' 'YooP8 8 8 8 8 8 8 'YooP8 88 + . 8 + 'oooP' + +Please use this tool with care to avoid data damage or loss! + +There is no function to undo the changes done by this tool, so you +should be aware of what you are doing. Improper use (e. g. modifying +files inside system directories) will corrupt your system! + +If you wish to proceed, type '%s' (case-sensitive, without any +quotes or spaces) and press the key. Otherwise, the process +will be cancelled.""" % string + + print_text_box("", notice_text) + choice = input("> ") + + if choice == string: + choice = "Proceeding." + proceed = True + else: + choice = "cancelled." + print("\n%s\n" % choice) + + return proceed + +def dir_space_modifier(directory, remove_duplicate=False, + remove_leading=False, remove_trailing=False, + brackets=False, hyphens=False, punctuation=False, + ignore_symlinks=False, recursive=False, exclude=None): + """ + Modify a directory name by removing leading, trailing and duplicate + spaces or by inserting and removing spaces next to punctuation + characters. + """ + + list_exclude = [] + if not exclude == None: + list_exclude = exclude.split(";") + + for item in os.listdir(directory): + excluded = False + if os.path.isdir(os.path.join(directory, item)): + path = os.path.join(directory, item) + for excl in list_exclude: + if excl.lower() in path.lower(): + excluded = True + break + else: + continue + + if excluded: + nextdir = path + else: + if remove_duplicate: + while (" " * 2) in item: + item = item.replace((" " * 2), " ") + + if hyphens: + item = item.replace("-", " - ") + while "- " in item: + item = item.replace("- ", "- ") + while " -" in item: + item = item.replace(" -", " -") + + if brackets: + while "( " in item: + item = item.replace("( ", "(") + item = item.replace("(", " (") + while " )" in item: + item = item.replace(" )", ")") + item = item.replace(")", ") ").replace(") .", ").") + while "[ " in item: + item = item.replace("[ ", "[") + item = item.replace("[", " [") + while " ]" in item: + item = item.replace(" ]", "]") + item = item.replace("]", "] ").replace("] .", "].") + item = item.replace("( [", "([").replace("] )", "])") + item = item.replace("[ (", "[(").replace(") ]", ")]") + + if punctuation: + item = item.replace(".", ". ") + while " ." in item: + item = item.replace(" .", ".") + item = item.replace(",", ", ") + while " ," in item: + item = item.replace(" ,", ",") + item = item.replace(":", ": ") + while " :" in item: + item = item.replace(" :", ":") + item = item.replace(";", "; ") + while " ;" in item: + item = item.replace(" ;", ";") + item = item.replace("!", "! ") + while " !" in item: + item = item.replace(" !", "!") + item = item.replace("?", "? ") + while " ?" in item: + item = item.replace(" ?", "?") + remove_leading = True + remove_trailing = True + + if remove_leading: + item = item.lstrip() + if remove_trailing: + item = item.rstrip() + + newpath = os.path.join(directory, item) + if remove_duplicate: + # Repeat this step after the replace actions above + while (" " * 2) in newpath: + newpath = newpath.replace((" " * 2), " ") + + if not os.path.exists(newpath): + os.rename(path, newpath) + nextdir = newpath + else: + nextdir = path + + if recursive: + dir_space_modifier(nextdir, remove_duplicate, remove_leading, + remove_trailing, brackets, hyphens, + punctuation, ignore_symlinks, True, exclude) + +def file_exists(file_path, list_files, fs_case_sensitive): + """ + Check if a file already exists on the file system as well as in a + given list. + """ + file_path = os.path.abspath(file_path) + if os.path.exists(file_path): + file_exists = True + else: + file_exists = False + + for item in list_files: + if item[1] == None: + item[1] = "" + + if fs_case_sensitive: + if file_path == item[1] or file_path == item[2]: + file_exists = True + break + else: + if file_path.lower() == item[1].lower() or \ + file_path.lower() == item[2].lower(): + file_exists = True + break + + return file_exists + +def format_timestamp(float_stamp=0): + """ + Convert a timestamp float into a readable format. + """ + return str(dt.fromtimestamp(float(str(float_stamp)))) + +def get_files(directory, recursive=False, ignore_case=True, regex=None, + regex_exclude=True, ignore_symlinks=False, order_by=None): + """ + Get the files and sub-directories from the given directory. + """ + pv.path(directory, "given", False, True) + + directory = os.path.abspath(directory) + list_files = [] + list_excluded = [] + + list_files, list_excluded = \ + __get_files( \ + directory, ignore_case, regex, regex_exclude, ignore_symlinks, + recursive, list_files, list_excluded, order_by) + + if order_by == None: + list_files.sort() + list_excluded.sort() + + return list_files, list_excluded + +def get_fs_case_sensitivity(directory): + """ + Determine if the file system of the given directory is case-sensitive. + """ + # This should be done with every directory that is processed, due to the + # fact, that e. g. a device containing a case-insensitive file system can + # be mounted into a directory of a case-sensitive file system. + + pv.path(directory, "given", False, True) + directory = os.path.abspath(directory) + + fd_temp, file_temp = tempfile.mkstemp(dir=directory) + file_name = os.path.basename(file_temp) + if os.path.exists(os.path.join(directory, file_name.upper())): + fs_case_sensitive = False + else: + fs_case_sensitive = True + os.close(fd_temp) + os.remove(file_temp) + + return fs_case_sensitive + +def get_invalid_chars(): + """ + Return the invalid file name characters (which must or should not be + part of a file name). + """ + # This list of characters depends on the file system where the files are + # being renamed on. Due to the fact, that e. g. a device containing a + # different file system can be mounted into a directory of the local file + # system, the following characters will be handled as invalid on every + # file system. + invalid_chars = "/\\?%*:|\"<>\n\r\t" + + return invalid_chars + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def print_text_box(heading, text): + """ + Print a text message outlined with an ASCII character frame. + """ + heading = heading.strip() + if len(heading) > 72: + raise Exception("The text box heading must not be longer than 72 " \ + "characters.") + if text == "": + raise Exception("The text box text must not be empty.") + + text_box = "\n+" + ("-" * 76) + "+" + \ + "\n|" + (" " * 76) + "|" + if not heading == "": + padding = int((72 - len(heading)) / 2) + heading = (" " * (padding + 2) + heading).ljust(76, " ") + text_box += ("\n|%s|\n|" + (" " * 76) + "|") % heading + list_text = text.split("\n") + for text in list_text: + list_words = text.split(" ") + count = 1 + line = "" + for word in list_words: + if len(line + word + " ") > 73: + text_box += "\n| " + line.ljust(74, " ") + "|" + line = word + " " + else: + line = line + word + " " + count += 1 + if count > len(list_words): + text_box += "\n| " + line.ljust(74, " ") + "|" + text_box += "\n|" + (" " * 76) + "|" \ + "\n+" + ("-" * 76) + "+\n" + + print(text_box) + +def random_string(length, uppercase=True, lowercase=False, numbers=False, + unique=False): + """ + Generate a random string out of literals and numbers. + """ + literals = "ABCDEFGHIJLMNOPQRSTUVWXYZ" + numbers = "0123456789" + chars = "" + string = "" + + if uppercase: + chars += literals + if lowercase: + chars += literals.lower() + if numbers: + chars += numbers + + if len(chars) == 0: + return string + if len(chars) < length: + length = len(chars) + + while len(string) < length: + rnd = random.randint(0, len(chars) - 1) + char = chars[rnd] + if unique: + if char in string: + continue + string += char + + return string + + +def rename(list_files, reverse=False): + """ + Rename the files which have neither been excluded nor skipped. + """ + list_skipped = [] + + if len(list_files) > 0: + if reverse: + list_files = reversed(list_files) + + for item in list_files: + if os.path.exists(item[0]): + if os.path.exists(item[2]): + list_skipped.append([item[0], item[1], item[2]]) + continue + + # In some cases the file will get a temporary name first and + # then its name will be changed to what it should be. + # + # This behaviour is required when using file systems that are + # case-insensitive (such as FAT32 or NTFS) where e. g. the + # file "FOOBAR.txt" would overwrite the file "foobar.txt" + # inside the same directory. + if item[1] == None or \ + item[1] == "": + os.rename(item[0], item[2]) + else: + os.rename(item[0], item[1]) + os.rename(item[1], item[2]) + + if len(list_skipped) > 0: + if not list_skipped == list_files: + rename(list_skipped, reverse) + +def report(report_file=None, list_header=[], list_renamed=[], + list_excluded=[], list_skipped=[], time_start=None): + """ + Write the details of the simulated rename process (simulation report) + into a file. + """ + files_total = str(len(list_renamed) + len(list_excluded) + \ + len(list_skipped)) + just = len(files_total) + files_renamed = str(len(list_renamed)).rjust(just, " ") + files_excluded = str(len(list_excluded)).rjust(just, " ") + files_skipped = str(len(list_skipped)).rjust(just, " ") + time_end = dt.now() + + try: + time_elapsed = str(time_end - time_start) + time_start = str(time_start) + except: + raise Exception("An invalid start date was given.") + + output = "\r\n" + "=" * 78 + \ + "\r\nFile type: " + list_header[0] + \ + "\r\n" + "-" * 78 + + for i in range(1, len(list_header)): + output += "\r\n" + list_header[i][0].ljust(20, " ") + \ + str(list_header[i][1]) + + output += "\r\n" + "-" * 78 + \ + "\r\nFiles renamed: " + files_renamed + \ + "\r\nFiles excluded: " + files_excluded + \ + "\r\nFiles skipped: " + files_skipped + \ + "\r\nFiles total: " + files_total + \ + "\r\n" + "-" * 78 + \ + "\r\nTimestamp: " + time_start[:-7] + \ + "\r\nElapsed time: " + time_elapsed + \ + "\r\nNomen version: " + __version__ + \ + "\r\n" + "=" * 78 + "\r\n\r\n" + + if len(list_renamed) > 0: + output += "\r\n [Renamed]\r\n" + for item in list_renamed: + output += " - Old: %s\r\n" % item[0] + output += " - New: %s\r\n\r\n" % item[2] + output += "\r\n" + + if len(list_excluded) > 0: + output += "\r\n [Excluded]\r\n" + for item in list_excluded: + output += " - %s\r\n" % item + output += "\r\n" + + if len(list_skipped) > 0: + output += "\r\n [Skipped]\r\n" + for item in list_skipped: + output += " - %s\r\n" % item + output += "\r\n" + + fh_report = open(report_file, "wb") + + # Run the appropriate code for the Python framework used + if sys.version_info[0] == 2: + fh_report.write(output) + elif sys.version_info[0] > 2: + fh_report.write(output.encode(sys.getdefaultencoding())) + + fh_report.close() + +def __get_files(directory, ignore_case, regex, regex_exclude, ignore_symlinks, + recursive, list_content, list_excluded, order_by): + """ + Core method to get the files from the given directory and its + sub-directories. + """ + list_dirs = [] + list_files = [] + + for item in os.listdir(directory): + path = os.path.join(directory, item) + if ignore_symlinks: + if os.path.islink(path): + continue + if os.path.isfile(path): + if regex == None: + list_files.append(path) + else: + if regex_exclude: + if ignore_case: + if regex.match(item.lower()): + list_excluded.append(path) + else: + list_files.append(path) + else: + if regex.match(item): + list_excluded.append(path) + else: + list_files.append(path) + else: + if ignore_case: + if regex.match(item.lower()): + list_files.append(path) + else: + list_excluded.append(path) + else: + if regex.match(item): + list_files.append(path) + else: + list_excluded.append(path) + else: + list_dirs.append(path) + + if len(list_files) > 0: + if order_by == None: + list_files.sort() + else: + list_files = __set_order(list_files, order_by) + list_content.append([directory, list_files]) + + if recursive: + for directory in list_dirs: + list_content, list_excluded = \ + __get_files(directory, ignore_case, regex, regex_exclude, + ignore_symlinks, True, list_content, + list_excluded, order_by) + + return list_content, list_excluded + +def __set_order(file_list, order_by): + """ + Set a certain order of the files before renaming them. + """ + list_files = [] + list_temp = [] + + for file_name in file_list: + if "." in file_name: + file_ext = file_name.split(".")[-1] + else: + file_ext = "" + + time_access = format_timestamp(os.stat(file_name).st_atime) + time_create = format_timestamp(os.stat(file_name).st_ctime) + time_modify = format_timestamp(os.stat(file_name).st_mtime) + + if order_by == "accessed": + list_temp.append([time_access, file_name, file_ext]) + elif order_by == "created": + list_temp.append([time_create, file_name, file_ext]) + else: + list_temp.append([time_modify, file_name, file_ext]) + + list_temp.sort() + for item in list_temp: + list_files.append(item[1]) + + return list_files + +# EOF + diff --git a/python3/core/extren.py b/python3/core/extren.py new file mode 100755 index 0000000..0457d44 --- /dev/null +++ b/python3/core/extren.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Extension renamer core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +from . import common +import os +from . import paval as pv +import re + +from datetime import datetime as dt + +def convert_case(directory, case, conflict_mode, recursive=False, + report_file=None, ignore_symlinks=False): + """ + Convert the case of the file extensions. + """ + pv.path(directory, "given", False, True) + pv.compstr(case, "case", ["lower", "title", "upper"]) + pv.compstr(conflict_mode, "conflict mode", ["rename", "skip"]) + + case = case.lower() + conflict_mode = conflict_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = re.compile("(.*)") + + list_content, list_excluded = \ + common.get_files(directory, recursive, True, regex, False, + ignore_symlinks) + for item in list_content: + list_files = item[1] + list_renamed, list_skipped = \ + __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive) + + if simulate: + list_header = [] + list_header.append("Nomen Extension Case Converter simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Conflict mode:", conflict_mode.capitalize()]) + list_header.append(["Case:", case.capitalize()]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def rename_extensions(directory, conflict_mode, extension, extension_target, + recursive=False, ignore_case=True, report_file=None, + ignore_symlinks=False): + """ + Rename the file extensions in the given directory and all of its + sub-directories (if requested). + """ + pv.path(directory, "given", False, True) + pv.compstr(conflict_mode, "conflict mode", ["rename", "skip"]) + pv.string(extension, "extension", False, common.get_invalid_chars()) + pv.string(extension_target, "target extension", False, + common.get_invalid_chars()) + + conflict_mode = conflict_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_extensions = [] + list_renamed = [] + list_skipped = [] + + if ";" in extension: + while (";" * 2) in extension: + extension = extension.replace((";" * 2), ";") + + list_temp = extension.split(";") + for extension in list_temp: + if not extension == "": + list_extensions.append(extension) + + if len(list_extensions) == 0: + raise Exception("The given extension list does not contain any " \ + "extensions.") + else: + list_extensions.append(extension) + + pattern = "" + for extension in list_extensions: + pattern += "(.*\." + str(extension) + "$)|" + pattern = pattern.rstrip("|") + + if ignore_case: + regex = re.compile(pattern, re.IGNORECASE) + else: + regex = re.compile(pattern) + + list_content, list_excluded = \ + common.get_files(directory, recursive, ignore_case, regex, False, + ignore_symlinks) + for item in list_content: + list_files = item[1] + list_renamed, list_skipped = \ + __rename_extensions(list_files, list_extensions, list_renamed, + list_skipped, conflict_mode, extension_target) + + if simulate: + list_header = [] + list_header.append("Nomen Extension Renamer simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Conflict mode:", conflict_mode.capitalize()]) + list_header.append(["Extensions:", extension]) + list_header.append(["Target extension:", extension_target]) + list_header.append(["Ignore case:", ignore_case]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive): + """ + Core method to convert the case of the file extensions. + """ + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + for file_path in list_files: + num = 1 + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + file_ext = os.path.splitext(file_name)[1] + + if file_ext == "": + list_skipped.append(file_path) + continue + + if case == "lower": + extension_target = file_ext.lower() + elif case == "title": + extension_target = file_ext.title() + elif case == "upper": + extension_target = file_ext.upper() + + file_newpath = file_path.replace(file_ext, extension_target) + if (file_path == file_newpath): + list_skipped.append(file_path) + continue + + if conflict_mode == "rename": + while True: + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if file_path.lower() == file_newpath.lower(): + break + + file_newpath = \ + file_path.replace(file_ext, + "_" + str(num) + extension_target) + num += 1 + else: + break + elif conflict_mode == "skip": + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if not file_path.lower() == file_newpath.lower(): + list_skipped.append(file_newpath) + continue + else: + list_skipped.append(file_newpath) + continue + + if os.path.exists(file_path): + list_renamed.append([file_path, file_newpath + ".__temp__", + file_newpath]) + + return list_renamed, list_skipped + +def __rename_extensions(list_files, list_extensions, list_renamed, + list_skipped, conflict_mode, extension_target): + """ + Core method to rename the file extensions. + """ + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + for file_path in list_files: + num = 1 + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + file_ext = os.path.splitext(file_name)[1] + + if file_ext == "": + list_skipped.append(file_path) + continue + + file_newpath = file_path.replace(file_ext, + os.path.extsep + extension_target) + if file_path == file_newpath: + list_skipped.append(file_path) + continue + + if conflict_mode == "rename": + while True: + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if file_path.lower() == file_newpath.lower(): + break + file_newpath = file_path.replace(file_ext, "_" + + str(num) + + os.path.extsep + + extension_target) + num += 1 + else: + break + elif conflict_mode == "skip": + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if not file_path.lower() == file_newpath.lower(): + list_skipped.append(file_path) + continue + else: + list_skipped.append(file_path) + continue + + if os.path.exists(file_path): + list_renamed.append([file_path, file_newpath + ".__temp__", + file_newpath]) + + return list_renamed, list_skipped + +# EOF + diff --git a/python3/core/fileren.py b/python3/core/fileren.py new file mode 100755 index 0000000..14a6038 --- /dev/null +++ b/python3/core/fileren.py @@ -0,0 +1,838 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File renamer core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +from . import common +import os +from . import paval as pv +import re +from . import statcase + +from datetime import datetime as dt + +def convert_case(directory, case, conflict_mode, recursive=False, + cfg_lower=None, cfg_mixed=None, cfg_title=None, + cfg_upper=None, report_file=None, ignore_symlinks=False): + """ + Convert the case of the base name of files. + """ + pv.path(directory, "given", False, True) + pv.compstr(case, "case", ["lower", "title", "upper", "config"]) + pv.compstr(conflict_mode, "conflict mode", ["rename", "skip"]) + + case = case.lower() + conflict_mode = conflict_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = re.compile("(.*)") + + __check_config_mismatch(case, cfg_lower, cfg_title, cfg_mixed, cfg_upper) + list_content, list_excluded = \ + common.get_files(directory, recursive, True, regex, False, + ignore_symlinks) + + for item in list_content: + list_files = item[1] + list_renamed, list_skipped = \ + __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive, cfg_lower, cfg_mixed, + cfg_title, cfg_upper) + + if simulate: + list_header = [] + list_header.append("Nomen File Name Case Converter simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Conflict mode:", conflict_mode.capitalize()]) + list_header.append(["Case:", case.capitalize()]) + list_header.append(["Lowercase config:", cfg_lower]) + list_header.append(["Mixed case config:", cfg_mixed]) + list_header.append(["Title case config:", cfg_title]) + list_header.append(["Uppercase config:", cfg_upper]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def modify_names(directory, action, position, input_string, + replace_string=None, recursive=False, exclude=None, + pattern=None, ignore_case=True, regex_syntax=False, + report_file=None, ignore_symlinks=False, strip_chars=None): + """ + Modify the base name of files by adding, removing or replacing a + user-defined string. + """ + pv.path(directory, "given", False, True) + pv.compstr(action, "action", ["add", "remove", "replace"]) + pv.compstr(position, "position", ["any", "prefix", "suffix"]) + pv.string(input_string, "input string", False, common.get_invalid_chars()) + + action = action.lower() + position = position.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + if not replace_string == None: + if not action == "replace": + raise Exception("The replace string argument can only be used " \ + "together with the action 'replace'.") + else: + pv.string(replace_string, "string False", False, + common.get_invalid_chars()) + + if action == "add" and position == "any": + raise Exception("The position 'any' cannot be used together with " \ + "the action 'add'.") + + if len(input_string) == 0: + raise Exception("The input string must not be empty.") + else: + pv.string(input_string, "input string", False, + common.get_invalid_chars()) + + if not strip_chars == None: + pv.string(strip_chars, "strip chars string", False, + common.get_invalid_chars()) + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = None + if not pattern == None: + regex = common.compile_regex(pattern, ignore_case, regex_syntax) + + list_content, list_excluded = \ + common.get_files(directory, recursive, ignore_case, regex, exclude, + ignore_symlinks) + for item in list_content: + list_files = item[1] + __modify_names(list_files, list_renamed, list_skipped, action, + position, input_string, replace_string, strip_chars) + + if simulate: + explicit = None + if exclude == None: + exclude = False + explicit = False + elif exclude: + explicit = False + else: + explicit = True + + list_header = [] + list_header.append("Nomen File Name Modifier simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Action to perform:", action.capitalize()]) + list_header.append(["Position:", position.capitalize()]) + list_header.append(["Input string:", "\"" + input_string + "\" " \ + "(without double quotes)"]) + if not replace_string == None: + list_header.append(["Replace string:", "\"" + replace_string + \ + "\" (without double quotes)"]) + if strip_chars == None: + list_header.append(["Strip chars:", "None"]) + else: + list_header.append(["Strip chars:", "\"" + strip_chars + "\" " \ + "(without double quotes)"]) + + list_header.append(["Exclude files:", exclude]) + list_header.append(["Explicit files:", explicit]) + list_header.append(["Pattern:", pattern]) + list_header.append(["Ignore case:", ignore_case]) + list_header.append(["Regex syntax:", regex_syntax]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def rename_files(directory, rename_mode, separator=" ", recursive=False, + padding=0, exclude=None, pattern=None, ignore_case=True, + regex_syntax=False, report_file=None, ignore_symlinks=False, + ignore_file_ext=False, custom_name=None, step=1, + order_by=None): + """ + Rename the base name of files based on the name of the directory where + they are stored in and add a numeric ID. + """ + pv.path(directory, "given", False, True) + pv.compstr(rename_mode, "rename mode", + ["fill-gaps", "increase", "keep-order", "rename-new"]) + pv.intrange(padding, "padding", 0, 12, True) + pv.string(separator, "seperator", False, common.get_invalid_chars()) + pv.intvalue(step, "step", True, False, False) + + if not order_by == None: + pv.compstr(order_by, "order by", ["accessed", "created", "modified"]) + if not rename_mode == "keep-order": + raise Exception("The order-by argument can only be used in " \ + "combination with keep-order mode.") + + step = int(step) + rename_mode = rename_mode.lower() + directory = os.path.abspath(directory) + if not directory.endswith(os.path.sep): + directory += os.path.sep + + if report_file == None: + simulate = False + else: + pv.path(report_file, "report", True, False) + report_file = os.path.abspath(report_file) + simulate = True + + if not custom_name == None: + pv.string(custom_name, "custom file name", False, + common.get_invalid_chars()) + + time_start = dt.now() + + list_content = [] + list_excluded = [] + list_renamed = [] + list_skipped = [] + regex = None + if not pattern == None: + regex = common.compile_regex(pattern, ignore_case, regex_syntax) + + list_content, list_excluded = \ + common.get_files(directory, recursive, ignore_case, regex, exclude, + ignore_symlinks, order_by) + + for item in list_content: + list_files = item[1] + if rename_mode == "fill-gaps": + list_renamed, list_skipped = \ + __rename_files_fill(list_files, list_renamed, list_skipped, + separator, padding, True, ignore_file_ext, + custom_name, step) + elif rename_mode == "rename-new": + list_renamed, list_skipped = \ + __rename_files_fill(list_files, list_renamed, list_skipped, + separator, padding, False, + ignore_file_ext, custom_name, step) + elif rename_mode == "keep-order": + list_renamed, list_skipped = \ + __rename_files_keep_order(list_files, list_renamed, + list_skipped, separator, padding, + ignore_file_ext, custom_name, step, + order_by) + else: + raise Exception("An invalid rename mode was given.") + + if simulate: + if padding == 0: + padding = "Set automatically" + else: + padding = str(padding) + + explicit = None + if exclude == None: + exclude = False + explicit = False + elif exclude: + explicit = False + else: + explicit = True + + if order_by == "accessed": + order_by = "Access time" + elif order_by == "created": + order_by = "Creation time" + elif order_by == "modified": + order_by = "Modification time" + else: + order_by = "False" + + list_header = [] + list_header.append("Nomen File Renamer simulation report") + list_header.append(["Report file name:", report_file]) + list_header.append(["Directory:", directory]) + list_header.append(["Recursive:", recursive]) + list_header.append(["Ignore symlinks:", ignore_symlinks]) + list_header.append(["Rename mode:", rename_mode.capitalize()]) + list_header.append(["Order by time:", order_by]) + list_header.append(["Separator:", "\"" + separator + "\" " \ + "(without double quotes)"]) + list_header.append(["Numeric padding:", padding]) + list_header.append(["Step size:", step]) + list_header.append(["Exclude files:", exclude]) + list_header.append(["Explicit files:", explicit]) + list_header.append(["Pattern:", pattern]) + list_header.append(["Ignore case:", ignore_case]) + list_header.append(["Regex syntax:", regex_syntax]) + + common.report(report_file, list_header, list_renamed, list_excluded, + list_skipped, time_start) + else: + common.rename(list_renamed) + +def __check_config_mismatch(case, cfg_lower, cfg_title, cfg_mixed, cfg_upper): + """ + Check for a case config mismatch. + """ + cfg_mismatch = False + + if case == "lower": + if not cfg_lower == None: + cfg_mismatch = True + elif case == "title": + if not cfg_title == None: + cfg_mismatch = True + elif case == "upper": + if not cfg_upper == None: + cfg_mismatch = True + elif case == "config": + if cfg_lower == None and \ + cfg_mixed == None and \ + cfg_title == None and \ + cfg_upper == None: + raise Exception("The config target case requires at least one " \ + "case config file to operate.") + else: + raise Exception("An unsupported case string was given.") + + if cfg_mismatch: + if case == "title": + case += " " + raise Exception("Config file mismatch (cannot use %scase config " \ + "file when using %scase as target case anyway)." % \ + (case, case)) + +def __convert_case(list_files, list_renamed, list_skipped, case, + conflict_mode, recursive, cfg_lower, cfg_mixed, cfg_title, + cfg_upper): + """ + Core method to convert the case of the base name of files. + """ + file_ext = "" + list_lower = [] + list_mixed = [] + list_title = [] + list_upper = [] + + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + if cfg_lower == None and cfg_mixed == None and \ + cfg_title == None and cfg_upper == None: + static_case = False + else: + list_lower, list_mixed, list_title, list_upper = \ + statcase.parse_case_configs(cfg_lower, cfg_mixed, cfg_title, + cfg_upper) + static_case = True + + for file_path in list_files: + num = 1 + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + base_name = os.path.splitext(file_name)[0] + if case == "lower": + base_name_target = base_name.lower() + elif case == "title": + base_name_target = base_name.title() + elif case == "upper": + base_name_target = base_name.upper() + else: + base_name_target = base_name + + if static_case: + base_name_target = __static_case(base_name_target, case, + list_lower, list_mixed, + list_title, + list_upper).rstrip() + + file_newpath = file_path.replace(base_name + file_ext, + base_name_target + file_ext) + if file_path == file_newpath: + list_skipped.append(file_path) + continue + + if conflict_mode == "rename": + while True: + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if file_path.lower() == file_newpath.lower(): + break + file_newpath = \ + file_path.replace(base_name, + base_name_target + "_" + str(num)) + num += 1 + else: + break + elif conflict_mode == "skip": + if common.file_exists(file_newpath, list_renamed, fs_case): + if not fs_case: + if not file_path.lower() == file_newpath.lower(): + list_skipped.append(file_path) + continue + else: + list_skipped.append(file_path) + continue + + if os.path.exists(file_path): + list_renamed.append([file_path, file_newpath + ".__temp__", + file_newpath]) + + return list_renamed, list_skipped + +def __fill_num_gaps(list_files, separator, padding, list_renamed, + list_skipped, fs_case, step): + """ + Core method to fill numeration gaps. + """ + list_temp = [] + list_temp.extend(list_skipped) + + for i in list_renamed: + list_temp.append(i[2]) + list_temp.sort() + list_gaps = __get_num_gaps(list_files, separator, padding, step) + + if len(list_gaps) > 0: + list_gaps.sort(reverse=True) + list_skipped.sort(reverse=True) + + while len(list_gaps) > 0: + if len(list_skipped) < 1: + break + + file_path = list_skipped.pop(0) + list_path = file_path.split(os.path.sep) + file_dir = list_path[-2] + file_name = list_path[-1] + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + num = list_gaps.pop(0) + file_num = str(num).rjust(int(padding), "0") + file_newname = file_dir + separator + \ + file_num.replace(" ", "0") + file_ext + file_newpath = file_path.replace(file_name, file_newname) + + if common.file_exists(file_newpath, list_renamed, fs_case): + list_skipped.append(file_path) + else: + list_renamed.append([file_path, None, file_newpath]) + + return list_renamed, list_skipped + +def __get_num_gaps(list_files, separator, padding, step): + """ + Method to determine numeration gaps. + """ + list_gaps = [] + + for i in range(len(list_files)): + x = (i + 1) * step + n = separator + str(x).rjust(int(padding),"0") + if not any(n in s for s in list_files): + list_gaps.append(int(n.replace(separator, ""))) + + return list_gaps + +def __modify_name_add(file_name, string, position): + """ + Core method to add a string to the base name of a file. + """ + file_newname = "" + + if position == "prefix": + file_newname = string + file_name + elif position == "suffix": + file_newname = file_name + string + + return file_newname + +def __modify_name_remove(file_name, string, position): + """ + Core method to remove a string from the base name of a file. + """ + file_newname = "" + + if position == "any": + file_newname = file_name.replace(string, "") + elif position == "prefix": + file_newname = re.sub("^" + string, "", file_name) + elif position == "suffix": + file_newname = re.sub(string + "$", "", file_name) + + return file_newname + +def __modify_name_replace(file_name, string, replace_string, position): + """ + Core method to replace a string inside the base name of a file. + """ + file_newname = "" + + if position == "any": + file_newname = file_name.replace(string, replace_string) + elif position == "prefix": + file_newname = re.sub("^" + string, replace_string, file_name) + elif position == "suffix": + file_newname = re.sub(string + "$", replace_string, file_name) + + return file_newname + +def __modify_names(list_files, list_renamed, list_skipped, action, position, + input_string, replace_string, strip_chars): + """ + Core method to modify the base name of files by adding, removing or + replacing a user-defined string. + """ + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + file_newname = "" + file_newpath = "" + + if os.path.extsep in file_name: + file_temp = os.path.splitext(file_name) + file_name = file_temp[0] + file_ext = file_temp[1] + else: + file_ext = "" + + if action == "add": + file_newname = __modify_name_add(file_name, input_string, + position) + elif action == "remove": + file_newname = __modify_name_remove(file_name, input_string, + position) + elif action == "replace": + file_newname = __modify_name_replace(file_name, input_string, + replace_string, position) + + if not strip_chars == None: + if len(strip_chars) > 0: + file_newname = file_newname.strip(strip_chars) + + file_newname += file_ext + file_newpath = file_path.replace(file_name + file_ext, file_newname) + if file_newpath == "": + list_skipped.append(file_path) + elif file_newname == "" or file_newname == file_ext: + list_skipped.append(file_path) + else: + if file_path == file_newpath: + list_skipped.append(file_path) + else: + if os.path.exists(file_newpath): + list_skipped.append(file_path) + else: + list_renamed.append([file_path, None, file_newpath]) + + return list_renamed, list_skipped + +def __process_case_list(case, case_list): + """ + Process a case list and separate words from regular expressions. + """ + list_strings = [] + list_words = [] + + for item in case_list: + if case == "lower": + if item.startswith("$("): + list_strings.append(item.replace("$", "").lower()) + else: + list_words.append(item.lower()) + elif case == "title": + if item.startswith("$("): + list_strings.append(item.replace("$", "").title()) + else: + list_words.append(item.title()) + elif case == "upper": + if item.startswith("$("): + list_strings.append(item.replace("$", "").upper()) + else: + list_words.append(item.upper()) + else: + if item.startswith("$("): + list_strings.append(item.replace("$", "")) + else: + list_words.append(item) + + return list_words, list_strings + +def __rename_files_fill(list_files, list_renamed, list_skipped, separator, + padding, fill_gaps=False, ignore_file_ext=False, + custom_name=None, step=1): + """ + Core method to rename the base name of files based on the name of the + directory where they are stored in using one of the "fill" rename + modes (such as "fill-gaps" and "rename-new"). + """ + file_newpath = "" + num = 0 + + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + if fill_gaps: + list_temp_renamed = [] + list_temp_skipped = [] + obj_ren = list_temp_renamed + obj_skip = list_temp_skipped + else: + obj_ren = list_renamed + obj_skip = list_skipped + + if padding == 0: + padding = len(str(len(list_files))) + + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + + if custom_name == None: + file_dir = list_path[-2] + else: + file_dir = custom_name + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + if file_name.startswith(file_dir + separator): + try: + temp = file_name.replace(file_dir + separator, "") + list_pad = temp.split(".") + file_padding = len(list_pad[0]) + + if step > 1: + if int(list_pad[0]) % step == 0: + obj_skip.append(file_path) + continue + else: + if int(padding) == file_padding: + obj_skip.append(file_path) + continue + except: + pass + + if not ignore_file_ext: + num = 0 + + file_newpath = file_path + while common.file_exists(file_newpath, obj_ren, fs_case) or \ + common.file_exists(file_newpath, obj_skip, fs_case): + num += step + file_num = str(num).rjust(int(padding), "0") + file_newname = \ + file_dir + separator + file_num.replace(" ", "0") + file_ext + file_newpath = file_path.replace(file_name, file_newname) + + if os.path.exists(file_path): + if file_path == file_newpath: + obj_skip.append(file_path) + else: + obj_ren.append([file_path, None, file_newpath]) + + if fill_gaps: + list_temp_renamed, list_temp_skipped = \ + __fill_num_gaps(list_files, separator, padding, + list_temp_renamed, list_temp_skipped, + fs_case, step) + list_renamed.extend(list_temp_renamed) + list_skipped.extend(list_temp_skipped) + + return list_renamed, list_skipped + +def __rename_files_keep_order(list_files, list_renamed, list_skipped, + separator, padding, ignore_file_ext=False, + custom_name=None, step=1, order_by=None): + """ + Core method to rename the base name of files based on the name of the + directory where they are stored in using "keep-order" rename mode. + """ + file_newpath = "" + file_temppath = "" + temp_file_ext = "" + list_new = [] + list_ren = [] + num = 0 + + fs_case = common.get_fs_case_sensitivity(os.path.dirname(list_files[0])) + + if padding == 0: + padding = len(str(len(list_files))) + + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_dir = list_path[-2] + file_name = list_path[-1] + + if file_name.startswith(file_dir + separator): + list_ren.append(file_path) + else: + list_new.append(file_path) + + list_files = [] + list_files.extend(list_ren) + list_files.extend(list_new) + + for file_path in list_files: + list_path = file_path.split(os.path.sep) + file_name = list_path[-1] + + if custom_name == None: + file_dir = list_path[-2] + else: + file_dir = custom_name + + if os.path.extsep in file_name: + file_ext = os.path.splitext(file_name)[1] + else: + file_ext = "" + + if not ignore_file_ext: + if not file_ext == temp_file_ext: + num = 0 + + file_temppath = file_path + temp_file_ext = file_ext + while common.file_exists(file_temppath, list_renamed, fs_case): + num += step + file_num = str(num).rjust(int(padding), "0") + file_newname = \ + file_dir + separator + file_num.replace(" ", "0") + file_ext + file_newpath = file_path.replace(file_name, file_newname) + if not file_newpath in list_skipped: + file_temppath = file_newpath + ".__temp__" + + if os.path.exists(file_path): + if file_path == file_newpath: + list_skipped.append(file_path) + else: + list_renamed.append([file_path, file_temppath, file_newpath]) + + return list_renamed, list_skipped + +def __static_case(base_name, case, list_lower, list_mixed, list_title, + list_upper): + """ + Convert the case of the base name of a file to lowercase, mixed case, + title case or uppercase based on the read out config files. + """ + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("lower", list_lower) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("mixed", list_mixed) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("title", list_title) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + list_file_str = base_name.split(" ") + list_cfg_str, list_cfg_regex = \ + __process_case_list("upper", list_upper) + base_name = __static_case_word(list_cfg_str, list_file_str) + base_name = __static_case_string(list_cfg_regex, base_name) + + return base_name + +def __static_case_word(list_cfg_str, list_file_str): + """ + Convert the case of each word of the base name of a file. + """ + base_name = "" + + for file_str in list_file_str: + for cfg_str in list_cfg_str: + spec_chars = "\\?*+$.:; ^|()[]{}" + for char in spec_chars: + cfg_str = cfg_str.replace(char, "") + + if file_str.lower() == cfg_str.lower(): + file_str = cfg_str + + base_name += (file_str + " ") + + return base_name + +def __static_case_string(list_cfg_regex, base_name): + """ + Convert the case of a string inside the base name of a file. + """ + for item in list_cfg_regex: + spec_chars = "\\?*+$.:;^|()[]{}" + for char in spec_chars: + item = item.replace(char, "") + + regex = re.compile(".*" + item.lower() + ".*") + if regex.match(base_name.lower()): + base_name = re.sub(item.lower(), item, base_name, + flags=re.IGNORECASE) + + return base_name + +# EOF + diff --git a/python3/core/paval.py b/python3/core/paval.py new file mode 100755 index 0000000..aba44a8 --- /dev/null +++ b/python3/core/paval.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# PaVal - Parameter validation module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/paval +# ============================================================================ + +__version__ = "1.2.7" + +import filecmp +import os + + +def compfile(input_path, name="", list_files=None): + """ + Compare files to avoid that the same file is given multiple times or + in different ways (e. g. different name but same content). + """ + __string(input_path, "%s path" % name, True) + + if list_files is None: + list_files = [] + elif not list_files: + __ex("File list is empty (no files to compare with).", True) + else: + for item in list_files: + if not isinstance(item, list): + __ex("Every list item must be a sub-list.", True) + if not len(item) == 2: + __ex("Every sub-list must contain two items.", True) + + input_path = os.path.abspath(input_path) + for item in list_files: + path_compare = os.path.abspath(str(item[0])) + name_compare = str(item[1]) + if input_path == path_compare: + __ex("The %s and the %s file path must not be identical." % + (name, name_compare), False) + if os.path.exists(input_path) and os.path.exists(path_compare): + if filecmp.cmp(input_path, path_compare, 0): + __ex("The %s and %s file content must not be identical." % + (name, name_compare), False) + + +def compstr(input_string, name="", list_strings=None): + """ + Compare a string with a list of strings and check if it is an item of + that list. + """ + __string(input_string, name, False) + if not list_strings: + __ex("No %s strings to compare with." % name, True) + if input_string not in list_strings: + __ex("The %s '%s' does not exist." % (name, input_string), False) + + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + + +def intrange(value, name="", value_min=None, value_max=None, zero=False): + """ + Validate an integer range. + """ + value = __integer(value, "%s value" % name, False) + if value_min is not None: + value_min = __integer(value_min, "minimal %s value" % name, True) + intvalue(value_min, name, True, True, True) + if value_max is not None: + value_max = __integer(value_max, "maximal %s value" % name, True) + intvalue(value_max, name, True, True, True) + if not zero: + if value == 0: + __ex("The %s value must not be zero." % name, False) + if (value_min is not None) and (value_max is not None): + if value_min > value_max: + __ex("The maximal %s value must be greater than the minimal " + "value." % name, False) + if (value_min == value_max) and (value != value_min): + __ex("The %s value can only be %s (depending on further range " + "further range arguments)." % (name, value_min), False) + if (value < value_min) or (value > value_max): + __ex("The %s value must be between %s and %s (depending on " + "further range arguments)." % (name, value_min, value_max), + False) + elif value_min is not None: + if value < value_min: + __ex("The %s value must not be less than %s." % (name, value_min), + False) + elif value_max is not None: + if value > value_max: + __ex("The %s value must not be greater than %s." % + (name, value_max), False) + + +def intvalue(value, name="", positive=True, zero=False, negative=False): + """ + Validate a single integer value. + """ + value = __integer(value, "%s value" % name, False) + if not positive: + if value > 0: + __ex("The %s value must not be positive." % name, False) + if not zero: + if value == 0: + __ex("The %s value must not be zero." % name, False) + if not negative: + if value < 0: + __ex("The %s value must not be negative." % name, False) + + +def path(input_path, name="", is_file=False, exists=False): + """ + Validate a path of a file or directory. + """ + string(input_path, "%s path" % name, False, None) + input_path = os.path.abspath(input_path) + + if is_file: + path_type = "file" + else: + path_type = "directory" + if exists: + if not os.path.exists(input_path): + __ex("The given %s %s does not exist." % (name, path_type), False) + if (is_file and not os.path.isfile(input_path)) or \ + (not is_file and not os.path.isdir(input_path)): + __ex("The given %s %s path is not a %s." % (name, path_type, + path_type), False) + else: + if os.path.exists(input_path): + __ex("The given %s %s path already exists." % (name, path_type), + False) + + +def string(input_string, name="", wildcards=False, invalid_chars=None): + """ + Validate a string. + """ + __string(input_string, name, False) + if invalid_chars is None: + invalid_chars = "" + if not wildcards: + if ("*" in input_string) or ("?" in input_string): + __ex("The %s must not contain wildcards." % name, False) + if invalid_chars: + for char in invalid_chars: + if char in input_string: + # Use single quotes by default or double quotes in case the + # single quotes are the invalid character + quotes = "'" + if char == quotes: + quotes = "\"" + + __ex("The %s contains at least one invalid character " + "(%s%s%s)." % (name, quotes, char, quotes), False) + + +def __ex(exception_string, internal=False): + """ + Internal method to raise an exception. + """ + ex = str(exception_string).strip() + while " " * 2 in ex: + ex = ex.replace((" " * 2), " ") + if internal: + ex = "PaVal: " + ex + raise Exception(ex) + + +def __integer(value, name="", internal=False): + """ + Internal method for basic integer validation. + """ + if value is None: + __ex("The %s is missing." % name, internal) + if value == "": + __ex("The %s must not be empty." % name, internal) + try: + value = int(value) + except ValueError: + __ex("The %s must be an integer." % name, internal) + return int(value) + + +def __string(input_string, name="", internal=False): + """ + Internal method for basic string validation. + """ + if input_string is None: + __ex("The %s is missing." % name, internal) + if input_string == "": + __ex("The %s must not be empty." % name, internal) + +# EOF diff --git a/python3/core/statcase.py b/python3/core/statcase.py new file mode 100755 index 0000000..551112d --- /dev/null +++ b/python3/core/statcase.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Static case core module +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +__version__ = "2.3.5" + +import os +from . import paval as pv +import sys + +def get_version(): + """ + Return the version of this module. + """ + return __version__ + +def parse_case_configs(cfg_lower=None, cfg_mixed=None, cfg_title=None, + cfg_upper=None): + """ + Parse the configuration files for static file name case + conversion. + """ + list_lower = [] + list_mixed = [] + list_title = [] + list_upper = [] + + if not cfg_lower == None: + list_lower = __read_config(__config_abspath(cfg_lower, "lowercase")) + if not cfg_mixed == None: + list_mixed = __read_config(__config_abspath(cfg_mixed, "mixed case")) + if not cfg_title == None: + list_title = __read_config(__config_abspath(cfg_title, "title case")) + if not cfg_upper == None: + list_upper = __read_config(__config_abspath(cfg_upper, "uppercase")) + + __check_dupes(list_lower, list_mixed, list_title, list_upper) + + return list_lower, list_mixed, list_title, list_upper + +def __check_dupes(list_lower, list_mixed, list_title, list_upper): + """ + Check for duplicate case list entries. + """ + list_diff = [] + + list_diff = __process_case_list(list_lower, list_diff) + list_diff = __process_case_list(list_mixed, list_diff) + list_diff = __process_case_list(list_title, list_diff) + __process_case_list(list_upper, list_diff) + +def __config_abspath(config, description): + """ + Get the absolute path of a config file. + """ + description += " config" + try: + pv.path(config, description, True, True) + except: + config_dir = os.path.abspath(os.path.dirname(sys.argv[0])) + config = os.path.join(config_dir, "cfg", os.path.basename(config)) + pv.path(config, description, True, True) + config = os.path.abspath(config) + + return config + +def __process_case_list(list_input, list_output): + """ + Process a case list and raise an exception in case of duplicate + entries. + """ + duplicate = "" + + for item in list_input: + if item == None or item == "": + continue + elif item.lower() in list_output: + duplicate = item + break + else: + list_output.append(item.lower()) + + if not duplicate == "": + raise Exception("Duplicate config file entries. The same string " \ + "must not exist in multiple config files. The " \ + "duplicate string was \"%s\" (without the " \ + "enclosing quotes)." % duplicate) + + return list_output + +def __read_config(config_file): + """ + Read out the contents of a config file. + """ + list_config = [] + + fh_config = open(config_file, "r") + for line in fh_config: + line = line.strip() + if line == "" or line.startswith("#"): + continue + list_config.append(line.replace("\n", "")) + fh_config.close() + list_config.sort + + return list_config + +# EOF + diff --git a/python3/docs/usage_nomen-dirspace.txt b/python3/docs/usage_nomen-dirspace.txt new file mode 100644 index 0000000..a91c061 --- /dev/null +++ b/python3/docs/usage_nomen-dirspace.txt @@ -0,0 +1,195 @@ + +USAGE (nomen-dirspace.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 2.5 Identical directory names + 3. Remove spaces from directory names + 3.1 Duplicate spaces + 3.2 Leading spaces + 3.3 Trailing spaces + 3.4 Duplicate, leading as well as trailing spaces + 4. Spaces next to certain characters + 4.1 Brackets + 4.2 Hyphens + 4.3 Punctuation characters + + 1. Definition + + The Nomen Directory Name Space Modifier (former "Nomen Directory Name + Space Remover") script modifies a directory name by removing leading, + trailing and duplicate spaces or by inserting and removing spaces next + to punctuation characters. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-dirspace.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + As a matter of fact, there is no function to undo the changes done + by this tool, so you should be aware of what you are doing. If + not, do NOT use this tool. + + Improper use (e. g. modifying system directories) will corrupt + your system! + + 2.4 Simulation mode + + Due to the fact that this tool simply removes unnecessary spaces + in directory names, it does not provide any simulation mode. + + 2.5 Identical directory names + + For example, if you have the directories + + /tmp/pics/My Holiday/ + /tmp/pics/My Holiday/ + + and want to remove the duplicate spaces, both directories would + have the same name after performing the space removal operation. + Due to this, they would be combined and the data inside of them + would get mixed up. + + To avoid this, the Nomen Directory Name Space Remover script does + not remove the spaces from a directory if another directory with + the new name already exists. The directory will simply be ignored, + but the script will process its sub-directories if the recursive + command-line argument was given. + + 3. Remove spaces from directory names + + The following operations can also be combined using the corresponding + command-line arguments (as described in section 3.4). + + 3.1 Duplicate spaces + + For example, if you have the following directory + + /tmp/pics/My Holiday/ + + and you want to remove the duplicate spaces, type: + + ./nomen-dirspace.py -d /tmp/pics/ -s + + 3.2 Leading spaces + + For example, if you have the following directory + + /tmp/pics/ My Holiday/ + + and you want to remove the leading spaces, type: + + ./nomen-dirspace.py -d /tmp/pics/ -l + + 3.3 Trailing spaces + + For example, if you have the following directory + + /tmp/pics/My Holiday / + + and you want to remove the trailing spaces, type: + + ./nomen-dirspace.py -d /tmp/pics/ -t + + 3.4 Duplicate, leading as well as trailing spaces + + For example, if you have the following directory + + /tmp/pics/ My Holiday / + + and you want to remove the duplicate, leading and trailing spaces, + type: + + ./nomen-dirspace.py -d /tmp/pics/ -s -l -t + + 4. Spaces next to certain characters + + The Nomen Directory Name Space Modifier also allows inserting and + removing spaces next to certain characters such as brackets, dashes + and punctuation characters. These are automated processes without any + further options. + + The following command-line arguments can be combined with those + mentioned in section 3 as well as with each other. + + Please notice that inserting spaces may result in duplicate spaces + again. So, when using the following command-line arguments it is + recommended to also add the "-s" (or "--remove-duplicate") argument + (as shown in the examples below). + + 4.1 Brackets + + The "-b" (or "--brackets") command-line argument will remove all + unnecessary spaces around brackets. For example, if you have a + directory like this + + /tmp/music/ Foo( Bar [ Album ] ) / + + and run Nomen Directory Name Space Modifier with the corresponding + argument + + ./nomen-dirspace.py -d /tmp/music/ -s -b + + the directory will be changed as follows: + + /tmp/music/Foo (Bar [Album])/ + + 4.2 Hyphens + + The --hyphens" command-line argument will add additional spaces + around hyphens ("-"). + + /tmp/music/ John Doe -Foo / + + and run Nomen Directory Name Space Modifier with the corresponding + argument + + ./nomen-dirspace.py -d /tmp/music/ -s -p + + the directory will be changed as follows: + + /tmp/music/John Doe - Foo/ + + 4.3 Punctuation characters + + The "-p" (or "--punctuation") command-line argument will remove + all unnecessary spaces around the punctuation characters such as + dots ("."), commas (","), colons (":"), semi-colons (";") as well + as exclamation ("!") and question marks ("?"). Furthermore, it + adds spaces next to them when necessary. + + /tmp/music/ Foo,Bar feat.John Doe - Foo ! / + + and run Nomen Directory Name Space Modifier with the corresponding + argument + + ./nomen-dirspace.py -d /tmp/music/ -s -p + + the directory will be changed as follows: + + /tmp/music/Foo, Bar feat. John Doe - Foo!/ + diff --git a/python3/docs/usage_nomen-extcase.txt b/python3/docs/usage_nomen-extcase.txt new file mode 100644 index 0000000..f75c612 --- /dev/null +++ b/python3/docs/usage_nomen-extcase.txt @@ -0,0 +1,126 @@ + +USAGE (nomen-extcase.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 3. Convert the case of file extensions + 3.1 Rename mode + 3.2 Skip mode + + 1. Definition + + The Nomen Extension Case Converter script converts the case of the + extension of all files inside a directory and (if requested) in all of + its sub-directories. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-extcase.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-extcase.py -m rename -c lower -d /tmp/pics/Holidays + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-extcase.py -m rename -c lower -d /tmp/pics/Holidays \ + --simulate /tmp/report.txt + + 3. Convert the case of file extensions + + For example, you have two picture files inside a directory called + "Holidays" with these extensions + + Holidays 1.JPG + Holidays 2.Jpg + + and you convert the extension case to lowercase, the file extensions + will look like this: + + Holidays 1.jpg + Holidays 2.jpg + + But, for example, if file system is case senstive and the directory + contains the files + + Holidays 1.JPG + Holidays 1.Jpg + + there would be a problem converting the file extension case, because + both target file names would be identical. So, to avoid that, the + script also offers two operating modes. + + 3.1 Rename mode + + The "rename mode" adds a numeric ID to the duplicate target file + and converts the file extension: + + Holidays 1.JPG will be renamed to "Holidays 1.jpg" + Holidays 1.Jpg will be renamed to "Holidays 1_1.jpg" + + To use this mode and convert the file extensions to lowercase, + type: + + ./nomen-extcase.py -m rename -c lower -d /tmp/pics/Holidays + + 3.2 Skip mode + + The "skip mode" simply skips the files where converting the case + of the file extension would result in duplicate file names: + + Holidays 1.JPG will be renamed to "Holidays 1.jpg" + Holidays 1.Jpg will be skipped + + To use this mode and convert the file extensions to lowercase, + type: + + ./nomen-extcase.py -m skip -c lower -d /tmp/pics/Holidays + diff --git a/python3/docs/usage_nomen-extren.txt b/python3/docs/usage_nomen-extren.txt new file mode 100644 index 0000000..33719ea --- /dev/null +++ b/python3/docs/usage_nomen-extren.txt @@ -0,0 +1,134 @@ + +USAGE (nomen-extren.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 3. Rename file extensions + 3.1 Rename mode + 3.2 Skip mode + + 1. Definition + + The Nomen Extension Renamer script renames (and adjusts) differently + spelled file extensions of the same file type file within a directory + and (if requested) in all of its sub-directories. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-extren.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-extren.py -m rename -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-extren.py -m rename -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" \ + --simulate /tmp/report.txt + + 3. Rename file extensions + + For example, you have four picture files inside a directory called + "Holidays" with these extensions: + + Holidays 1.jpg + Holidays 2.jpeg + Holidays 3.JPG + Holidays 4.Jpeg + + All of these files are JPEG pictures, but their extensions are spelled + differently. So, the Nomen Extension Renamer script can be used to get + consistent file extensions: + + Holidays 1.jpg + Holidays 2.jpg + Holidays 3.jpg + Holidays 4.jpg + + But, for example, if file system is case senstive and the directory + contains the files + + Holidays 1.Jpg + Holidays 1.jpeg + + there would be a problem renaming the file extension, because both + file target names would be identical. So, to avoid that, the script + also offers two operating modes. + + 3.1 Rename mode + + The "rename mode" adds a numeric ID to the duplicate target file + and converts the file: + + Holidays 1.Jpg will be renamed to "Holidays 1.jpg" + Holidays 1.jpeg will be renamed to "Holidays 1_1.jpg" + + To use this mode and rename the extensions of JPEG files, + type: + + ./nomen-extren.py -m rename -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" + + 3.2 Skip mode + + The "skip mode" simply skips the files where converting the case + of the file extension would result in duplicate file names: + + Holidays 1.Jpg will be renamed to "Holidays 1.jpg" + Holidays 1.jpeg will be skipped + + To use this mode and rename the extensions of JPEG files, type: + + ./nomen-extren.py -m skip -d /tmp/pics/Holidays \ + -e "JPG;JPEG" -t "jpg" + diff --git a/python3/docs/usage_nomen-filecase.txt b/python3/docs/usage_nomen-filecase.txt new file mode 100644 index 0000000..2af0fdc --- /dev/null +++ b/python3/docs/usage_nomen-filecase.txt @@ -0,0 +1,186 @@ + +USAGE (nomen-filecase.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 3. Convert the case of base names + 3.1 Rename mode + 3.2 Skip mode + 4. Using config files + 4.1 With a certain target case + 4.2 Without a certain target case + + 1. Definition + + The Nomen File Name Case Converter script converts the case of the + base name of all files inside a directory and (if requested) in all of + its sub-directories. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-filecase.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-filecase.py -m rename -c title -d /tmp/pics/Holidays + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-filecase.py -m rename -c title -d /tmp/pics/Holidays \ + --simulate /tmp/report.txt + + 3. Convert the case of base names + + For example, you have a picture file with this file name + + my favorite holiday.jpg + + and you convert the case of the base name to title case, the file name + will look like this: + + My Favorite Holiday.jpg + + But, for example, if file system is case senstive and the directory + contains the files + + HOLIDAY.jpg + holiday.jpg + + there would be a problem converting the case of the base name, because + both target file names would be identical. So, to avoid that, the + script also offers two operating modes. + + 3.1 Rename mode + + The "rename mode" adds a numeric ID to the base name of the + duplicate target file and converts the case: + + HOLIDAY.jpg will be renamed to "Holiday.jpg" + holiday.jpg will be renamed to "Holiday_1.jpg" + + To use this mode and convert the base name of the files to title + case, type: + + ./nomen-filecase.py -m rename -c title -d /tmp/pics/Holidays + + 3.2 Skip mode + + The "skip mode" simply skips the files where converting the case + of the base name would result in duplicate file names: + + HOLIDAY.jpg will be renamed to "Holiday.jpg" + holiday.jpg will be skipped + + To use this mode and convert the base name of the files to title + case, type: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays + + 4. Using config files + + The Nomen File Name Case Converter script also comes with the feature + that allows using config files to convert the case of certain strings + always to lowercase, title case, uppercase or a user-defined mixed + case, nomatter which target case (using the "-c" or "--case" argument) + was given. + + For details see the included sample config files which can be found + inside the "cfg" sub-directory. + + If the sample config file is missing for some reason, you may download + the project from the website: + + http://www.urbanware.org/nomen.html + + To use these config files, the corresponding command-line argument + followed by the full path to the config file is required, depending on + which of the config files you want to use. + + 4.1 With a certain target case + + For example, if you want to use title case as target case as well + as the lowercase config file named "lower.cfg" which is located + inside the directory "/tmp", the command line would look like + this: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg + + To additionally use the uppercase config file "upper.cfg" which + is also located inside the "/tmp" directory, simply add the + appropriate argument: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg \ + --cfg-upper /tmp/upper.cfg + + Same with the mixed case config: + + ./nomen-filecase.py -m skip -c title -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg \ + --cfg-mixed /tmp/mixed.cfg \ + --cfg-upper /tmp/upper.cfg + + However, you cannot use the title case config file when using + title case as target case anyway. + + 4.2 Without a certain target case + + If you just want to convert the case of the certain strings stored + inside the case config files, you can do this by giving "config" + as the target case: + + ./nomen-filecase.py -m skip -c config -d /tmp/pics/Holidays \ + --cfg-lower /tmp/lower.cfg \ + --cfg-mixed /tmp/mixed.cfg \ + --cfg-title /tmp/title.cfg \ + --cfg-upper /tmp/upper.cfg + diff --git a/python3/docs/usage_nomen-filemod.txt b/python3/docs/usage_nomen-filemod.txt new file mode 100644 index 0000000..5de39cd --- /dev/null +++ b/python3/docs/usage_nomen-filemod.txt @@ -0,0 +1,376 @@ + +USAGE (nomen-filemod.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 2.5 Prefixes and suffixes + 3. Add a user-defined string to file names + 3.1 Add a prefix + 3.2 Add a suffix + 4. Remove a user-defined string from file names + 4.1 Remove a prefix + 4.2 Remove a suffix + 4.3 Remove a string + 5. Replace a user-defined string inside file names + 5.1 Replace a prefix + 5.2 Replace a suffix + 5.3 Replace a string + 6. Strip file names + 7. Exclude certain files + 8. Rename certain files only + + 1. Definition + + The Nomen File Name Modifier script adds, removes or replaces a user- + defined string inside the base name of files. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-filemod.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-filemod.py -d /tmp/pics -a add -p prefix -s "Foobar" + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-filemod.py -d /tmp/pics -a add -p prefix -s "Foobar" \ + --simulate /tmp/report.txt + + 2.5 Prefixes and suffixes + + Typically a file name consists of a base name (prefix) and an + extension (suffix) separated by a separator, for example: + + holiday.jpg + + In this case, "holiday" is the base name, the dot is the separator + and "jpg" is the extension of the file. + + The Nomen File Name Modifier only modifies the base name of file + names. The "--position" argument expects either "any", "prefix" or + "suffix". These positions are only related to the base name, not + to the entire file name. + + For example, if you add the prefix "foo" to the base name it would + result in + + fooholiday.jpg + + and if you add the suffix "foo" (instead of the prefix) the base + name would look as follows: + + holidayfoo.jpg + + As already mentioned above, only the base name (the prefix of the + whole file name) will be modified and the file extension (the + suffix of the whole file name) will be ignored. + + 3. Add a user-defined string to file names + + Before modifying the base name of any files, please see section 2.5 + above. Notice that the given input string is processed case-sensitive. + + 3.1 Add a prefix + + For example, if you have the files + + holiday 1.jpg + holiday 2.jpg + holiday 3.jpg + holiday 4.jpg + + inside the directory "/tmp/pics" and want to add the string + "Summer " (without any quotes) as prefix to the base name of these + files, type: + + ./nomen-filemod.py -d /tmp/pics -a add -p prefix -s "Summer " + + This will rename the base name of the files as follows: + + Summer holiday 1.jpg + Summer holiday 2.jpg + Summer holiday 3.jpg + Summer holiday 4.jpg + + 3.2 Add a suffix + + For example, if you have the files + + Holiday 1.jpg + Holiday 2.jpg + Holiday 3.jpg + Holiday 4.jpg + + inside the directory "/tmp/pics" and want to add the string + " - Foobar" (without any quotes) as suffix to the base name of + these files, type: + + ./nomen-filemod.py -d /tmp/pics -a add -p suffix \ + -s " - Foobar" + + This will rename the base name of the files as follows: + + Holiday 1 - Foobar.jpg + Holiday 2 - Foobar.jpg + Holiday 3 - Foobar.jpg + Holiday 4 - Foobar.jpg + + 4. Remove a user-defined string from file names + + Before modifying the base name of any files, please see section 2.5 + above. Notice that the given input string is processed case-sensitive. + + 4.1 Remove a prefix + + For example, if you have the files + + Summer holiday 1.jpg + Summer holiday 2.jpg + Summer holiday 3.jpg + Summer holiday 4.jpg + + inside the directory "/tmp/pics" and want to remove the prefix + "Summer " (without any quotes) from the base name of these files, + type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p prefix \ + -s "Summer " + + This will rename the base name of the files as follows: + + holiday 1.jpg + holiday 2.jpg + holiday 3.jpg + holiday 4.jpg + + 4.2 Remove a suffix + + For example, if you have the files + + Holiday 1 - Foobar.jpg + Holiday 2 - Foobar.jpg + Holiday 3 - Foobar.jpg + Holiday 4 - Foobar.jpg + + inside the directory "/tmp/pics" and want to remove the suffix + " - Foobar" (without any quotes) from the base name of these + files, type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p suffix \ + -s " - Foobar" + + This will rename the base name of the files as follows: + + Holiday 1.jpg + Holiday 2.jpg + Holiday 3.jpg + Holiday 4.jpg + + 4.3 Remove a string + + This will remove all occurrences of the input string (no matter if + prefix, suffix or somewhere else) in the base name. + + For example, if you have the files + + Foobar Holiday Foobar 1.jpg + Foobar Holiday Foobar 2.jpg + Foobar Holiday Foobar 3.jpg + Foobar Holiday Foobar 4.jpg + + inside the directory "/tmp/pics" and want to remove the string + "Foobar " (without any quotes) from the base name of these files, + type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foobar " + + This will rename the base name of the files as follows: + + Holiday 1.jpg + Holiday 2.jpg + Holiday 3.jpg + Holiday 4.jpg + + 5. Replace a user-defined string inside file names + + Before modifying the base name of any files, please see section 2.5 + above. Notice that the given string (input and replace string) are + processed case-sensitive. + + 5.1 Replace a prefix + + For example, if you have the files + + Winter holiday 1.jpg + Winter holiday 2.jpg + Winter holiday 3.jpg + Winter holiday 4.jpg + + inside the directory "/tmp/pics" and want to replace the prefix + "Winter " with "Summer " (both without any quotes) inside the base + name of these files, type: + + ./nomen-filemod.py -d /tmp/pics -a replace -p prefix \ + -s "Winter " --replace-string "Summer " + + This will rename the base name of the files as follows: + + Summer holiday 1.jpg + Summer holiday 2.jpg + Summer holiday 3.jpg + Summer holiday 4.jpg + + 5.2 Replace a suffix + + For example, if you have the files + + Holiday 1 - Foobar.jpg + Holiday 2 - Foobar.jpg + Holiday 3 - Foobar.jpg + Holiday 4 - Foobar.jpg + + inside the directory "/tmp/pics" and want to replace the suffix + " - Foobar" with " Foo" (both without any quotes) inside the base + name of these files, type: + + ./nomen-filemod.py -d /tmp/pics -a replace -p suffix \ + -s " - Foobar" --replace-string " Foo" + + This will rename the base name of the files as follows: + + Holiday 1 Foo.jpg + Holiday 2 Foo.jpg + Holiday 3 Foo.jpg + Holiday 4 Foo.jpg + + 5.3 Replace a string + + This will replace all occurrences of the input string (no matter + if prefix, suffix or somewhere else) in the base name. + + For example, if you have the files + + Fu - Fubar - Fu 1.jpg + Fu - Fubar - Fu 2.jpg + Fu - Fubar - Fu 3.jpg + Fu - Fubar - Fu 4.jpg + + inside the directory "/tmp/pics" and want to replace the string + "Fu" with "Foo" (both without any quotes) inside the base name of + these files, type: + + ./nomen-filemod.py -d /tmp/pics -a replace -p any -s "Fu" \ + --replace-string "Foo" + + This will rename the base name of the files as follows: + + Foo - Foobar - Foo 1.jpg + Foo - Foobar - Foo 2.jpg + Foo - Foobar - Foo 3.jpg + Foo - Foobar - Foo 4.jpg + + 6. Strip file names + + You can also remove certain leading and trailing characters (such as + whitespaces) from the file name. For example, if you have the file + + Foo Holiday 1 Foo.jpg + + and want to remove the string "Foo" inside the entire file name, you + can do this using + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" + + but this would result in a file name with a leading as well as a + trailing whitespace (" Holiday 1 .jpg"). + + Now, there are multiple ways to remove these whitespaces. The first + way would be explicitly removing the prefix and suffix containing the + whitespace. + + ./nomen-filemod.py -d /tmp/pics -a remove -p prefix -s "Foo " + ./nomen-filemod.py -d /tmp/pics -a remove -p suffix -s " Foo" + + Another way would be removing the string "Foo" first and after that + the leading and trailing whitespaces separately. + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" + ./nomen-filemod.py -d /tmp/pics -a remove -p prefix -s " " + ./nomen-filemod.py -d /tmp/pics -a remove -p suffix -s " " + + The simplest way would be using the included command-line argument: + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" \ + --strip " " + + The "--strip" argument also allows giving multiple characters that + should be stripped. For example, to strip the letter "H" in uppercase + as well as whitespaces, type: + + ./nomen-filemod.py -d /tmp/pics -a remove -p any -s "Foo" \ + --strip "H " + + However, this will result in "oliday 1.jpg", due to the fact, that the + list of strip characters will not be processed in a certain order. + + 7. Exclude certain files + + See section 4 of the documentation of the Nomen File Renamer script + how to do this. The command-line arguments to exclude files used there + can also be applied to the Nomen File Modifier script. + + 8. Rename certain files only + + See section 5 of the documentation of the Nomen File Renamer script + how to do this. The command-line arguments to rename files only used + there can also be applied to the Nomen File Modifier script. + diff --git a/python3/docs/usage_nomen-fileren.txt b/python3/docs/usage_nomen-fileren.txt new file mode 100644 index 0000000..321188f --- /dev/null +++ b/python3/docs/usage_nomen-fileren.txt @@ -0,0 +1,496 @@ + +USAGE (nomen-fileren.py) + + Contents: + + 1. Definition + 2. General stuff + 2.1 How to run Python scripts + 2.2 Overview of all command-line arguments + 2.3 Warning + 2.4 Simulation mode + 2.5 Significant changes (released with version 2.1.0) + 2.5.1 Names of the rename modes + 2.5.2 Way of renaming files + 3. Rename files + 3.1 Using "keep-order" mode + 3.1.1 Order by name (default) + 3.1.2 Order by access time + 3.1.3 Order by creation time + 3.1.4 Order by modification time + 3.2 Using "rename-new" mode + 3.3 Using "fill-gaps" mode + 4. Exclude certain files + 4.1 Exclude certain files using basic strings + 4.2 Exclude certain files using wildcards + 5. Rename certain files only + 5.1 Rename certain files using basic strings + 5.2 Rename certain files using wildcards + 6. Steps between each numeric ID + 6.1 Issue before + 6.2 Solution + + 1. Definition + + The Nomen File Renamer script renames the base name of files within a + directory and (if requested) in all of its sub-directories based on + the name of the directory where the files are stored in and adds a + unique numeric ID. + + 2. General stuff + + 2.1 How to run Python scripts + + All usage examples below show how to execute the Python scripts on + the shell of a Unix-like system. If you do not know, how to run + those scripts on your operating system, you may have a look at + this page: + + http://www.urbanware.org/howto_python.html + + 2.2 Overview of all command-line arguments + + Usually, each script requires command-line arguments to operate. + So, to get an overview of all arguments available, simply run the + script with the "--help" argument. For example: + + $ ./nomen-fileren.py --help + + 2.3 Warning + + Please use this tool with care to avoid data damage or loss! + + It is strongly recommended to simulate every rename process first + to check which files would have been renamed. As a matter of fact, + there is no function to undo the changes done by this tool, so you + should be aware of what you are doing. If not, do NOT use this + tool. + + Improper use (e. g. modifying files inside system directories) + will corrupt your system! + + 2.4 Simulation mode + + As already mentioned above, before renaming any files, you should + simulate the rename process by using the "--simulate" argument. + + This argument requires a file path where the report of the rename + process will be written to. When in simulation mode, no files will + be renamed at all. + + So, if you have a command line like this + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays + + but want to simulate the rename process and write the report into + the file "/tmp/report.txt", simply add the argument + + --simulate /tmp/report.txt + + to the command line: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + --simulate /tmp/report.txt + + 2.5 Significant changes (released with version 2.1.0) + + 2.5.1 Names of the rename modes + + Before Nomen version 2.0.0, there only were two rename modes, + "consecutive" and "fill". + + In version 2.0.0 the "fill" mode has been forked into the two + modes "fillin" and "fillnew". + + With the release of version 2.1.0, all these names have been + replaced with new ones for more clarity. + + +--------------------------+--------------------------+ + | Before | After | + +--------------------------+--------------------------+ + | consecutive | keep-order | + | fillin | fill-gaps | + | fillnew | rename-new | + +--------------------------+--------------------------+ + + 2.5.2 Way of renaming files + + In the past, the files have been renamed with a continuous + number ignoring the file type. + + With the release of version 2.1.0, by default the file types + will not longer be ignored, so every file type has its own + range of numbers: + + +--------------------------+--------------------------+ + | Before | After | + +--------------------------+--------------------------+ + | Picture_0001.bmp | Picture_0001.bmp | + | Picture_0002.bmp | Picture_0002.bmp | + | Picture_0003.jpg | Picture_0001.jpg | + | Picture_0004.jpg | Picture_0002.jpg | + | Picture_0005.png | Picture_0001.png | + | Picture_0006.png | Picture_0002.png | + +--------------------------+--------------------------+ + + However, with the "--ignore-file-ext" command-line argument + this behaviour can be changed, so the files will be renamed + with a continuous number ignoring the file type again. + + 3. Rename files + + For example, if you have four picture files inside the directory + "/tmp/pics/Holidays": + + pic_0001.jpg + pic_0002.jpg + pic_0003.jpg + pic_0004.jpg + + Basically, the Nomen File Renamer script will rename the files inside + that directory based on the directory name where they are stored in + and add a unique numeric ID. For example: + + Holidays 1.jpg + Holidays 2.jpg + Holidays 3.jpg + Holidays 4.jpg + + The script offers some file rename modes. + + 3.1 Using "keep-order" mode + + The "keep-order" mode keeps the chronological order even if files + have been deleted. There are multiple ways this mode can be used. + + 3.1.1 Order by name (default) + + In case no order command-line argument was given, the default + behaviour will be used. For example, if you delete the picture + file "Holidays 2.jpg" and re-run the script with this rename + mode it will do the following: + + Holidays 1.jpg stays (no need to rename) + Holidays 3.jpg will be renamed to "Holidays 2.jpg" + Holidays 4.jpg will be renamed to "Holidays 3.jpg" + + Now, if you add another file to that directory, "foobar.jpg" + for example, and re-run the Nomen File Renamer script with + this rename mode, the new file will be appended to the + existing and already renamed files: + + Holidays 1.jpg stays (no need to rename) + Holidays 2.jpg stays (no need to rename) + Holidays 3.jpg stays (no need to rename) + foobar.jpg will be renamed to "Holidays 4.jpg" + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays + + In case you want to reorganize these pictures, put them into a + new directory called "Summer" among with some other files and + re-run the script with this rename mode, the files will not be + renamed chronologically, but alphabetically. For example: + + Beach 1.jpg will be renamed to "Summer 1.jpg" + Beach 2.jpg will be renamed to "Summer 2.jpg" + Holidays 1.jpg will be renamed to "Summer 3.jpg" + Holidays 2.jpg will be renamed to "Summer 4.jpg" + Holidays 3.jpg will be renamed to "Summer 5.jpg" + Holidays 4.jpg will be renamed to "Summer 6.jpg" + + So, even if the "Beach" pictures are newer than the "Holidays" + pictures the files will be renamed like this. + + You can also keep a certain order, for example, beginning with + the "Holidays" pictures followed by the "Beach" pictures. At + first, put only the "Holidays" pictures into the "Summer" + directory and run the script with this rename mode: + + Holidays 1.jpg will be renamed to "Summer 1.jpg" + Holidays 2.jpg will be renamed to "Summer 2.jpg" + Holidays 3.jpg will be renamed to "Summer 3.jpg" + Holidays 4.jpg will be renamed to "Summer 4.jpg" + + After that, put the "Beach" pictures in the "Summer" directory + and run the script with this mode again: + + Summer 1.jpg stays (no need to rename) + Summer 2.jpg stays (no need to rename) + Summer 3.jpg stays (no need to rename) + Summer 4.jpg stays (no need to rename) + Beach 1.jpg will be renamed to "Summer 5.jpg" + Beach 2.jpg will be renamed to "Summer 6.jpg" + + 3.1.2 Order by access time + + Please read section 3.1.1 above first before continuing with + this one. + + You can also rename and order the files by the time they have + been accessed in ascending order. So, the file with the oldest + time will be the first one to be renamed. + + However, the access time is not being determined by Nomen, it + is the timestamp provided by the metadata of the file. + + Notice that this will irreversibly change the order of the + files. + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + -o accessed + + 3.1.3 Order by creation time + + Please read section 3.1.1 above first before continuing with + this one. + + You can also rename and order the files by the time they have + been created in ascending order. So, the file with the oldest + time will be the first one to be renamed. + + However, the creation time is not being determined by Nomen, + it is the timestamp provided by the metadata of the file. + + Notice that this will irreversibly change the order of the + files. + + According to the official Python documentation (see "Common + pathname manipulations"), the creation time information + depends on the operation system. + + On some systems (like Unix) it is the time of the last change + of the metadata, and, on others (like Windows), is the + creation time of the file. + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + -o created + + 3.1.4 Order by modification time + + Please read section 3.1.1 above first before continuing with + this one. + + You can also rename and order the files by the time they have + been modified in ascending order. So, the file with the oldest + time will be the first one to be renamed. + + However, the modification time is not being determined by + Nomen, it is the timestamp provided by the metadata of the + file. + + Notice that this will irreversibly change the order of the + files. + + To use this rename mode, type: + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + -o modified + + 3.2 Using "rename-new" mode + + The "rename-new" mode does NOT keep the chronological order if + files have been deleted. It simply fills the numeration gaps with + new files or files that have been renamed manually. + + For example, if you delete the picture file "Holidays 2.jpg" and + re-run the Nomen File Renamer script with this rename mode, no + file names will be changed. + + Holidays 1.jpg stays (no need to rename) + Holidays 3.jpg stays (no need to rename) + Holidays 4.jpg stays (no need to rename) + + If you then add another file to that directory, e. g. "foobar.jpg" + and re-run the script this rename mode again, it will simply fill + the gap "Holiday 2.jpg" has left using the new file: + + Holidays 1.jpg stays (no need to rename) + Holidays 3.jpg stays (no need to rename) + Holidays 4.jpg stays (no need to rename) + foobar.jpg will be renamed to "Holidays 2.jpg" + + To use this rename mode, type: + + ./nomen-fileren.py -m rename-new -d /tmp/pics/Holidays + + 3.3 Using "fill-gaps" mode + + The "fill-gaps" mode works like the "rename-new" mode (for details + see section 3.2) with the difference, that it fills all numeration + gaps using new as well as the existing files with the greatest + numbers. For example: + + Holidays 1.jpg stays (no need to rename) + Holidays 4.jpg stays (no need to rename) + Holidays 5.jpg will be renamed to "Holidays 3.jpg" + foobar.jpg will be renamed to "Holidays 2.jpg" + + To use this rename mode, type: + + ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays + + 4. Exclude certain files + + 4.1 Exclude certain files using basic strings + + For example, to exclude all files whose file name (base name or + extension) contains the string "foo", type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "foo" + + By default, the given pattern is being processed case-insensitive. + That means, the string given above would match "foo" as well as + e. g. "FOO", "Foo", "FoO" and so on. + + The pattern can also consist of multiple strings (separated with + semicolons): + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "foo2;foo4" + + 4.2 Exclude certain files using wildcards + + The pattern also supports asterisk wildcards. So, if you want to + exclude all files whose file name (base name or extension) + contains a string that starts with "foo" and ends with "bar" + (e. g. "foobar", "foo-bar" or "foo bar"), type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "foo*bar" + + The pattern can also consist of multiple strings (separated with + semicolons) with wildcards: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --exclude "f*o;b*r" + + 5. Rename certain files only + + 5.1 Rename certain files using basic strings + + So, for example, to only rename the files whose file name (base + name or extension) contains the string "foo", type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "foo" + + By default, the given pattern is being processed case-insensitive. + That means, the string given above would match "foo" as well as + e. g. "FOO", "Foo", "FoO" and so on. + + The pattern can also consist of multiple strings (separated with + semicolons): + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "foo2;foo4" + + 5.2 Rename certain files using wildcards + + The pattern also supports asterisk wildcards. So, if you want to + only rename the files whose file name (base name or extension) + contains a string that starts with "foo" and ends with "bar" + (e. g. "foobar", "foo-bar" or "foo bar"), type: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "foo*bar" + + The pattern can also consist of multiple strings (separated with + semicolons) with wildcards: + + $ ./nomen-fileren.py -m fill-gaps -d /tmp/pics/Holidays \ + --explicit "f*o;b*r" + + 6. Steps between each numeric ID + + Since version 2.3.0, it is also possible to set an additional user- + defined step value, which can be combined with all rename modes + mentioned in section 3 above. + + Especially when using the the "keep-order" mode, adding new files to + existing ones between two numeric IDs was quite inconvenient. Below + you can find an example issue and its solution. + + 6.1 Issue before + + There is a directory containing already renamed files and a new + one that should be between others like this + + Holidays 1.jpg + Holidays 2.jpg + Holidays 3.jpg + pic_0001.jpg should actually be "Holidays 2.jpg" + + and running + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays + + would rename "pic_0001.jpg" to "Holidays 4.jpg", which is not + what we want. + + One way to solve that is: + + 1. Create a new temporary sub-directory with a different name, + for example "Temp". + + 2. Move all files whose names are starting with "Holdays" into + that directory. + + 3. Then, run the Nomen File Renamer script again. The file + "pic_0001.jpg" file will then renamed to "Holidays 2.jpg". + + 4. In case that the target directory has not been processed + recursively before, run the script once again with the path + of the sub-directory as target directory, so the term + "Holidays" inside the file name will be replaced by "Temp". + + 5. Move all files from the temporary directory back into the + directory where the "Holidays" files are. + + 6. Delete the temporary directory. + + 7. Now, run the Nomen File Renamer script a third time with + the path of the directory that contains the "Holidays" + files as the target directory again. + + 6.2 Solution + + Now, before adding new files that should be between others, run + the Nomen File Renamer script (before adding new files) with a + user-defined step value, for example 10. + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + --step 10 + + This will rename the files as follows: + + Holidays 10.jpg + Holidays 20.jpg + Holidays 30.jpg + + Then add the "pic_0001.jpg" file to that directiry and rename it + like "Holidays 11.jpg" (or with any number between 10 and 20). + + Holidays 10.jpg stays (no need to rename) + Holidays 11.jpg will be renamed to "Holidays 20.jpg" + Holidays 20.jpg will be renamed to "Holidays 30.jpg" + Holidays 30.jpg will be renamed to "Holidays 3.jpg" + + Now, run the Nomen File Renamer script once again + + ./nomen-fileren.py -m keep-order -d /tmp/pics/Holidays \ + --step 10 + + to apply the changes: + + Holidays 10.jpg stayed (no need to rename) + Holidays 20.jpg was "Holidays 11.jpg" before + Holidays 30.jpg was "Holidays 20.jpg" before + Holidays 40.jpg was "Holidays 30.jpg" before + diff --git a/python3/license.txt b/python3/license.txt new file mode 100644 index 0000000..5c861af --- /dev/null +++ b/python3/license.txt @@ -0,0 +1,27 @@ + +LICENSE (Nomen) + + Nomen - Multi-purpose rename tool + Copyright (C) 2018 by Ralf Kilian + + Distributed under the MIT License: + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/python3/nomen-dirspace.py b/python3/nomen-dirspace.py new file mode 100755 index 0000000..0b85eb7 --- /dev/null +++ b/python3/nomen-dirspace.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Directory name space modifier script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print("%s: error: %s" % (os.path.basename(sys.argv[0]), e)) + sys.exit(1) + + p.set_description("Modify a directory name by removing leading, " \ + "trailing and duplicate spaces or by inserting and " \ + "removing spaces next to punctuation characters.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + + # Optional arguments + p.add_switch("-b", "--brackets", "insert and remove spaces next to " \ + "brackets", "brackets", True, False) + p.add_avalue(None, "--exclude", "pattern to exclude certain files " \ + "(case-insensitive, multiple patterns separated via " \ + "semicolon)", "exclude_pattern", None, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--hyphens", "insert spaces next to hyphens", \ + "hyphens", True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-l", "--remove-leading", "remove leading spaces", + "remove_leading", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_switch("-p", "--punctuation", "insert and remove spaces next to " \ + "punctuation characters", "punctuation", True, False) + p.add_switch("-s", "--remove-duplicate", "remove duplicate spaces", + "remove_duplicate", True, False) + p.add_switch("-t", "--remove-trailing", "remove trailing spaces", + "remove_trailing", True, False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print(fileren.get_version()) + sys.exit(0) + + try: + args = p.parse_args() + if not args.punctuation and not args.remove_duplicate and \ + not args.remove_leading and not args.remove_trailing: + p.error("Nothing to do (no optional arguments were given).") + + common.dir_space_modifier(args.directory, args.remove_duplicate, + args.remove_leading, args.remove_trailing, + args.brackets, args.hyphens, + args.punctuation, args.ignore_symlinks, + args.recursive, args.exclude_pattern) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python3/nomen-extcase.py b/python3/nomen-extcase.py new file mode 100755 index 0000000..78af59e --- /dev/null +++ b/python3/nomen-extcase.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Extension case converter script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import extren + + try: + p = clap.Parser() + except Exception as e: + print("%s: error: %s" % (os.path.basename(sys.argv[0]), e)) + sys.exit(1) + + p.set_description("Convert the case of the extension of all files " \ + "inside a directory and (if requested) in all of its " \ + "sub-directories.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_predef("-c", "--case", "target extension case", "case", + ["lower", "title", "upper"], True) + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-m", "--conflict-mode", "conflict mode (in case of " \ + "duplicate file names)", "conflict_mode", ["rename", "skip"], + True) + + # Optional arguments + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print(extren.get_version()) + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + extren.convert_case(args.directory, args.case, args.conflict_mode, + args.recursive, args.report_file, + args.ignore_symlinks) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python3/nomen-extren.py b/python3/nomen-extren.py new file mode 100755 index 0000000..fdf6e33 --- /dev/null +++ b/python3/nomen-extren.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# Extension renamer script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import extren + + try: + p = clap.Parser() + except Exception as e: + print("%s: error: %s" % (os.path.basename(sys.argv[0]), e)) + sys.exit(1) + + p.set_description("Rename (and adjust) differently spelled file " \ + "extensions of the same file type file within a " \ + "directory and (if requested) in all of its sub-" \ + "directories.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_avalue("-e", "--extension", "extension to rename (case-" \ + "sensitive, multiple extensions separated via semicolon)", + "extension", None, True) + p.add_predef("-m", "--conflict-mode", "conflict mode (in case of " \ + "duplicate file names)", "conflict_mode", ["rename", "skip"], + True) + p.add_avalue("-t", "--target-extension", "target extension (case-" \ + "sensitive)", "extension_target", None, True) + + # Optional arguments + p.add_switch("-c", "--case-sensitive", "do not ignore the case of the " \ + "given extension list", "case", False, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print(extren.get_version()) + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + extren.rename_extensions(args.directory, args.conflict_mode, + args.extension, args.extension_target, + args.recursive, args.case, args.report_file, + args.ignore_symlinks) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python3/nomen-filecase.py b/python3/nomen-filecase.py new file mode 100755 index 0000000..6ad55dd --- /dev/null +++ b/python3/nomen-filecase.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File name case converter script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print("%s: error: %s" % (os.path.basename(sys.argv[0]), e)) + sys.exit(1) + + p.set_description("Convert the case of the base name of all files " \ + "inside a directory and (if requested) in all of its " \ + "sub-directories.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_predef("-c", "--case", "target case of the base name", "case", + ["lower", "title", "upper", "config"], True) + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-m", "--conflict-mode", "conflict mode (in case of " \ + "duplicate file names)", "conflict_mode", ["rename", "skip"], + True) + + # Optional arguments + p.add_avalue(None, "--cfg-lower", "path to the config file for strings " \ + "which should always be lowercase inside the file " \ + "name", "cfg_lower", None, False) + p.add_avalue(None, "--cfg-mixed", "path to the config file for strings " \ + "which should always be mixed case inside the " \ + "file name", "cfg_mixed", None, False) + p.add_avalue(None, "--cfg-title", "path to the config file for strings " \ + "which should always be title case inside the " \ + "file name", "cfg_title", None, False) + p.add_avalue(None, "--cfg-upper", "path to the config file for strings " \ + "which should always be uppercase inside the file " \ + "name", "cfg_upper", None, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print(fileren.get_version()) + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + fileren.convert_case(args.directory, args.case, args.conflict_mode, + args.recursive, args.cfg_lower, args.cfg_mixed, + args.cfg_title, args.cfg_upper, args.report_file, + args.ignore_symlinks) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python3/nomen-filemod.py b/python3/nomen-filemod.py new file mode 100755 index 0000000..63b303e --- /dev/null +++ b/python3/nomen-filemod.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File name modifier script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print("%s: error: %s" % (os.path.basename(sys.argv[0]), e)) + sys.exit(1) + + p.set_description("Modify the base name of files by adding, removing " \ + "or replacing a user-defined string.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_predef("-a", "--action", "action to perform", "action", + ["add", "remove", "replace"], True) + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-p", "--position", "position where to perform the action", + "position", ["any", "prefix", "suffix"], True) + p.add_avalue("-s", "--string", "input string to perform the action " \ + "with (case-sensitive)", "input_string", None, True) + + # Optional arguments + p.add_switch("-c", "--case-sensitive", "do not ignore the case of the " \ + "given exclude or explicit pattern", "case", False, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_avalue(None, "--exclude", "pattern to exclude certain files " \ + "(case-insensitive, multiple patterns separated via " \ + "semicolon)", "exclude_pattern", None, False) + p.add_avalue(None, "--explicit", "explicit pattern to only process " \ + "certain files (case-insensitive, multiple patterns " \ + "separated via semicolon)", "explicit_pattern", None, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_switch(None, "--regex", "use regex syntax for the exclude or " \ + "explicit pattern instead of just asterisk wildcards and " \ + "semicolon separators (for details see the section " \ + "'Regular expression operations' inside the official " \ + "Python documentation)", "regex_syntax", True, False) + p.add_avalue(None, "--replace-string", "string to replace the input" \ + "string with (when using the action 'replace')", + "replace_string", None, False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_avalue(None, "--strip", "remove certain leading and trailing " \ + "characters from the base name", "strip_chars", None, False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print(fileren.get_version()) + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + if args.exclude_pattern and args.explicit_pattern: + p.error("The exclude and the explicit pattern argument cannot be " \ + "given at the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + if args.exclude_pattern: + pattern = args.exclude_pattern + exclude = True + elif args.explicit_pattern: + exclude = False + pattern = args.explicit_pattern + else: + exclude = None + pattern = None + + fileren.modify_names(args.directory, args.action, args.position, + args.input_string, args.replace_string, + args.recursive, exclude, pattern, args.case, + args.regex_syntax, args.report_file, + args.ignore_symlinks, args.strip_chars) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python3/nomen-fileren.py b/python3/nomen-fileren.py new file mode 100755 index 0000000..9308e31 --- /dev/null +++ b/python3/nomen-fileren.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# ============================================================================ +# Nomen - Multi-purpose rename tool +# File renamer script +# Copyright (C) 2018 by Ralf Kilian +# Distributed under the MIT License (https://opensource.org/licenses/MIT) +# +# Website: http://www.urbanware.org +# GitHub: https://github.com/urbanware-org/nomen +# ============================================================================ + +import os +import sys + +def main(): + from core import clap + from core import common + from core import fileren + + try: + p = clap.Parser() + except Exception as e: + print("%s: error: %s" % (os.path.basename(sys.argv[0]), e)) + sys.exit(1) + + p.set_description("Rename the base name of files within a directory " \ + "and (if requested) in all of its sub-directories " \ + "based on the name of the directory where the files " \ + "are stored in and add a unique numeric ID.") + p.set_epilog("Further information and usage examples can be found " \ + "inside the documentation file for this script.") + + # Required arguments + p.add_avalue("-d", "--directory", "directory that contains the files " \ + "to process", "directory", None, True) + p.add_predef("-m", "--rename-mode", "rename mode to use", "rename_mode", + ["fill-gaps", "increase", "keep-order", "rename-new"], True) + + # Optional arguments + p.add_switch("-c", "--case-sensitive", "do not ignore the case of the " \ + "given exclude or explicit pattern", "case", False, False) + p.add_switch(None, "--confirm", "skip the confirmation prompt and " \ + "instantly rename files", "confirm", True, False) + p.add_avalue(None, "--custom-name", "custom file name (instead of the " \ + "directory name where the files are stored in)", + "custom_name", None, False) + p.add_avalue(None, "--exclude", "pattern to exclude certain files " \ + "(case-insensitive, multiple patterns separated via " \ + "semicolon)", "exclude_pattern", None, False) + p.add_avalue(None, "--explicit", "explicit pattern to only process " \ + "certain files (case-insensitive, multiple patterns " \ + "separated via semicolon)", "explicit_pattern", None, False) + p.add_switch("-h", "--help", "print this help message and exit", None, + True, False) + p.add_switch(None, "--ignore-file-ext", "ignore file extensions when " \ + "numerating files", "ignore_file_ext", True, False) + p.add_switch(None, "--ignore-symlinks", "ignore symbolic links", + "ignore_symlinks", True, False) + p.add_predef("-o", "--order-by", "order files by last accessed, " \ + "created or modified date", "order_by", ["accessed", + "created", "modified"], False) + p.add_avalue("-p", "--padding", "set a user-defined numeric padding " \ + "(if no user-defined padding value is given, it will be " \ + "set automatically based on the amount of files per " \ + "directory)", "padding", 0, False) + p.add_switch("-r", "--recursive", "process the given directory " \ + "recursively", "recursive", True, False) + p.add_switch(None, "--regex", "use regex syntax for the exclude or " \ + "explicit pattern instead of just asterisk wildcards and " \ + "semicolon separators (for details see the section " \ + "'Regular expression operations' inside the official " \ + "Python documentation)", "regex_syntax", True, False) + p.add_avalue("-s", "--separator", "use a user-defined character or " \ + "string as a separator between the directory name and the " \ + "unique numeric ID", "separator", " ", False) + p.add_avalue(None, "--simulate", "simulate the rename process and " \ + "write the details into a report file", "report_file", None, + False) + p.add_avalue(None, "--step", "steps between each numeric ID", "step", 1, + False) + p.add_switch(None, "--version", "print the version number and exit", None, + True, False) + + if len(sys.argv) == 1: + p.error("At least one required argument is missing.") + elif ("-h" in sys.argv) or ("--help" in sys.argv): + p.print_help() + sys.exit(0) + elif "--version" in sys.argv: + print(fileren.get_version()) + sys.exit(0) + + args = p.parse_args() + if args.confirm and not args.report_file == None: + p.error("The confirm and the simulate argument cannot be given at " \ + "the same time.") + if args.exclude_pattern and args.explicit_pattern: + p.error("The exclude and the explicit pattern argument cannot be " \ + "given at the same time.") + + try: + if not args.confirm and args.report_file == None: + if not common.confirm_notice(): + sys.exit(0) + + if args.exclude_pattern: + pattern = args.exclude_pattern + exclude = True + elif args.explicit_pattern: + exclude = False + pattern = args.explicit_pattern + else: + exclude = None + pattern = None + + fileren.rename_files(args.directory, args.rename_mode, args.separator, + args.recursive, args.padding, exclude, pattern, + args.case, args.regex_syntax, args.report_file, + args.ignore_symlinks, args.ignore_file_ext, + args.custom_name, args.step, args.order_by) + except Exception as e: + p.error(e) + +if __name__ == "__main__": + main() + +# EOF + diff --git a/python3/readme.txt b/python3/readme.txt new file mode 100644 index 0000000..e4b8fba --- /dev/null +++ b/python3/readme.txt @@ -0,0 +1,67 @@ + +README (Nomen) + + Project + + Nomen + Version 2.3.5 (based on Python framework 3.x) + Copyright (C) 2018 by Ralf Kilian + + Website: http://www.urbanware.org + GitHub: https://github.com/urbanware-org/nomen + + Definition + + The Nomen project is a multi-purpose rename tool to consistently + rename the base name as well as the extension of files in a variety of + ways and also to remove unnecessary whitespaces from directory names. + + License + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Requirements + + The code of this Nomen version was simply converted from Python 2.x + to 3.x syntax using the 2to3 tool and has only been tested + fundamentally. + + In order to use the project, Python 3.2 or higher is recommended, but + it may also work with earlier versions. + + If you need a version that works with Python 2.x, you can also find + one on the website of the project mentioned above. + + Usage + + For fundamental documentation as well as some usage examples for each + component of the project, you may have a look at the text files inside + the included 'docs' sub-directory. + + Legal information + + The project name is completely fictitious. Any correspondences with + existing websites, applications, companies and/or other projects are + purely coincidental. + + All trademarks belong to their respective owners. + + Errors and omissions excepted. +