PK%G3Z@Yaka/__init__.py""" Copyright (C) 2015 Mattias Ugelvik This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import pathlib import sys from aka.utils import temporary_directory, yes_no_prompt, expand_stuff from aka.actions import RenameActions, CopyActions RENAME_PROMPT = """ The files will be renamed as shown above (in two passes though, in order to avoid collisions). This program searched for name conflicts in all target directories and did not find any. If errors do pop up, you'll be taken to an emergency mode where you can roll back changes. Continue?""" COPY_PROMPT = """ The files will be copied as shown above. This program searched for name conflicts in all target directories and did not find any. If errors do pop up, you'll be taken to an emergency mode where you can roll back changes. Continue?""" def perform_actions(machine, location, prompt, emergency, pass_dirname, action_class, prompt_text, temp_dir="/dummy"): assert emergency in {None, "continue", "rollback", "exit"} actions = action_class(machine, location, temp_dir, pass_dirname, emergency) if not actions.actions: print("No files to {}".format(actions.verb)) return True elif not actions.report_conflicts(): if not prompt: return actions.do_all() else: actions.show_actions() actions.print_targets() if yes_no_prompt(prompt_text, default=False): return actions.do_all() else: print("Aborting...", file=sys.stderr) return False def rename(machine, location=".", prompt=True, emergency=None, pass_dirname=False): """ Renames files in the directory `location`, returning True on success, False if problems are detected (like naming conflicts). It can also return False if emergency mode is entered and the user selects "exit" ,"rollback" or "continue" (although not "retry"; that is considered a success). Every filename will be passed to the callable `machine` which should give the new name for the given file. If `machine` returns a falsy value then the filename will be ignored (i.e., not renamed). If `dirname` then `machine` will be called with `machine(fn, dirname)` where `fn` is the filename as usual and where `dirname` is the absolute path to the directory in which `fn` is located. The filename `machine` returns can point outside of `location`, in which case the file will be moved. If it's a relative path, it will be relative with respect to `location`. A prompt will appear asking the user whether `rename` should proceed if `prompt` is truthy. `emergency` specifies which action to be taken in case of an emergency. By default it will query the user, but you can set it to "rollback" or "continue" or "exit". If an error occurs in the process of renaming, then you will be put in emergency mode where you can rollback changes; continue; exit the program; or retry. If you choose "continue" or "exit" in emergency mode then some files will be left behind in the temporary directory; the location of this directory will be printed before exit. """ with temporary_directory() as temp_dir: return perform_actions(machine, location, prompt, emergency, pass_dirname, RenameActions, RENAME_PROMPT, temp_dir) def copy(machine, location=".", prompt=True, emergency=None, pass_dirname=False): """ Like `aka.rename`, but copies files instead. """ return perform_actions(machine, location, prompt, emergency, pass_dirname, CopyActions, COPY_PROMPT) PK&GIKs&s&aka/actions.py""" Copyright (C) 2015 Mattias Ugelvik This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import itertools import collections import shutil import pathlib import sys import os from aka.utils import (expand_stuff, find_duplicates, prompt_user) def print_actions(actions): if actions: size = min(45, max(len(repr(action[0])) for action in actions)) for old, new in actions: print(" {!s: <{}} -> {}".format(old, size, new)) EMERGENCY_PROMPT = """What should the program do? retry : try again (presumably you've fixed something in the meantime) rollback : attempt to undo changes (except for the ones previously continue'd) showroll : show which actions will be taken if you choose `rollback` exit : exit the program continue : ignore the error and move on > """ class Actions: def __init__(self, machine, basedir, temp_dir, pass_dirname=False, emergency_default=None): self.emergency_default = emergency_default self.basedir = pathlib.Path(expand_stuff(basedir)).absolute().resolve() self.changed = {} # Files in `basedir` which are "selected" by `machine` self.unchanged = set() # The rest of the files in `basedir` self.actions = [] # ^ List of two-tuples, signifying actions, but only sublasses of Actions understands # how to use them. Note that RenameActions.make_action temporarily fills in triples, # but they are flattened later in RenameActions.__init__ for old in self.basedir.iterdir(): result = machine(old.name, str(old.parent)) if pass_dirname else machine(old.name) if result: normalized = self.basedir / pathlib.PurePath(expand_stuff(result)) new = normalized.parent.resolve() / normalized.name if old == new: self.unchanged.add(old) continue # pointless to rename in this case self.changed[old] = new self.actions.append( self.make_action(old, new, pathlib.Path(temp_dir)) # temp_dir can be a dummy value ) else: self.unchanged.add(old) self.mapping = collections.defaultdict(dict) # Used for checking conflicts for old, new in self.changed.items(): self.mapping[new.parent][old] = new self.continued_indices = set() # Keeping track of continued actions for smooth rollbacks def error(self, message, label="ERROR"): print("{}: {}".format(label, message), file=sys.stderr) def print_targets(self): print("Target directories:") for directory in self.mapping: print(" {}".format(directory)) def find_conflicts(self): for directory in self.mapping: yield from self.conflicts_in_dir( directory, blacklist=self.make_blacklist(directory) ) if not os.access(str(directory), os.X_OK | os.W_OK): yield "Target directory {} is not writable (and/or executable) by you!".format(directory) def report_conflicts(self): """ Returns True if there are conflicts, False otherwise, and reports conflicts to the user as a side effect. """ return bool([self.error(c) for c in self.find_conflicts()]) def conflicts_in_dir(self, directory, blacklist): changes = self.mapping[directory] for old, new in changes.items(): """ `new` is the new name of the file, pointing into the dir in question. `blacklist` are "occupied" paths, obviously consisting of all the files in the dir in question, but it will omit files set for renaming. """ if new in blacklist: yield "{} -> {} is a conflict!".format(old, new) dups = find_duplicates(changes.values()) if dups: yield "There are duplicates among the new proposed names: {}".format(dups) def do_all(self, do_indices=None, rollback=False): """ Returns False on failure, True on success. """ if do_indices is None: do_indices = range(len(self.actions)) for index in do_indices: while True: try: (self.undo if rollback else self.do)(*self.actions[index]) break except Exception as error: if rollback: self.error("Error {} in rollback; ignoring and continuing".format(error)) break else: self.error(error, label="\n\nEMERGENCY MODE") self.error("Error happened when trying to {} {} -> {}\n".format(self.verb, *self.actions[index])) rollbacks = tuple(idx for idx in reversed(range(index)) if idx not in self.continued_indices) tactic = (self.emergency_default or prompt_user(EMERGENCY_PROMPT, {"rollback", "exit", "continue", "retry"}, callbacks={ "showroll": lambda: self.showroll(rollbacks) }, file=sys.stderr)) if tactic == "rollback": self.do_all(rollbacks, True) return False elif tactic == "continue": self.continued_indices.add(index) break # out of the while loop to move on to the next index elif tactic == "retry": continue # the while loop to try again elif tactic == "exit": return False # False means failure # Failure if it had to continue an index (but a retry counts as a success) return not bool(self.continued_indices) class RenameActions(Actions): verb = "rename" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # self.actions is now a list of triples, but I need them to be two-tuples. fst, snd = [], [] self.simplified_actions = [] # for printing for old, temp, new in self.actions: fst.append( (old, temp) ) snd.append( (temp, new) ) self.simplified_actions.append( (old, new) ) self.actions = fst + snd # Making sure the actions happens in the right order def showroll(self, rollbacks): print("Rollback actions:") print_actions([tuple(reversed(self.actions[idx])) for idx in rollbacks]) def show_actions(self): print("Actions to be taken (simplified; doesn't show the temporary stage):") print_actions(self.simplified_actions) def do(self, old, new): print("Renaming {} -> {}".format(old, new)) if new.exists(): raise Exception("File {} already exists!".format(new)) old.rename(new) def undo(self, old, new): print("Rollback renaming {} -> {}".format(new, old)) if old.exists(): raise Exception("File {} already exists!".format(new)) new.rename(old) def make_action(self, old, new, temp_dir): return (old, temp_dir/old.name, new) def make_blacklist(self, directory): # In `basedir`, the changed files are "free" so to speak, for reusage return self.unchanged if directory == self.basedir else set(directory.iterdir()) def conflicts_in_dir(self, directory, blacklist): yield from super().conflicts_in_dir(directory, blacklist) if any(parent in self.changed for parent in itertools.chain(directory.parents, [directory])): yield "Target dir {} (or any of its parents) is also about to be moved, that doesn't make sense".format(directory) class CopyActions(Actions): verb = "copy" def showroll(self, rollbacks): print("Rollback actions:") for old, new in (self.actions[idx] for idx in rollbacks): print(" Delete {}".format(new)) def show_actions(self): print("Actions to be taken:") print_actions(self.actions) def do(self, old, new): print("Copying {} -> {}".format(old, new)) if new.exists(): raise Exception("File {} already exists!".format(new)) (shutil.copytree if old.is_dir() else shutil.copy2)(str(old), str(new)) def undo(self, old, new): print("Removing {}".format(new)) (shutil.rmtree if new.is_dir() else os.remove)(str(new)) def make_action(self, old, new, temp_dir): return (old, new) # Doesn't need the temp_dir def make_blacklist(self, directory): return set(directory.iterdir()) PKR&G$Ru aka/utils.py""" Copyright (C) 2015 Mattias Ugelvik This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import contextlib import tempfile import pathlib import sys import os def expand_stuff(string): return os.path.expandvars(os.path.expanduser(string)) def find_duplicates(iterable): dups, seen = set(), set() for item in iterable: if item in seen: dups.add(item) seen.add(item) return dups @contextlib.contextmanager def temporary_directory(): """ Make a temporary directory. Will remove it afterwards, if it is empty. """ temp_dir = tempfile.mkdtemp(prefix="aka_") try: yield temp_dir finally: try: os.rmdir(temp_dir) except OSError as e: print("LOST FILES IN TEMP DIR: {!r}".format(temp_dir), file=sys.stderr) def prompt_user(prompt, alternatives, default=None, callbacks=dict(), file=sys.stdout): while True: print(prompt ,file=file, end="") response = input().lower() if response in alternatives: return response elif response in callbacks: callbacks[response]() continue elif default and response == "": return default print("Sorry, you must choose one: {!r}".format(alternatives), file=file) def yes_no_prompt(prompt, default=True): """ Returns True if the user responds "yes", False otherwise """ return prompt_user( "{} {}: ".format(prompt, "[Y/n]" if default else "[N/y]"), alternatives={"y", "yes", "n", "no"}, default="y" if default else "n") in {"y", "yes"} PKۖ)G9,,aka-1.0.data/scripts/aka#!python """ Copyright (C) 2015 Mattias Ugelvik This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import aka, sys, contex, os, re, shlex, subprocess, tempfile, argparse from importlib.machinery import SourceFileLoader # Windows seems to always crash when I print hearts. That's because Windows is against love. EXAMPLES = '{0}Examples{0}:'.format("" if sys.platform == "win32" else " ♥ ♥ ♥ ") + ''' Append ".jpg" to all files in the current directory: $ aka -p 'fn + ".jpg"' aka will not rename automatically, it will ask you first to confirm that the changes look right. If you want to test your expression first without the risk of hitting the wrong button, you can replace -p with -t. Turn all filenames in ~/Documents such as File3.txt into File4.txt: $ aka -p 'rules(r"File(?P\d+)\.txt", {"num": lambda num: int(num) + 1})' ~/Documents The above example works because `rules` returns a unary `callable`. Any files that do not match will be ignored, because `rules` returns `None` in that case, which is a falsy value. Open an editor to write more complicated renaming functions: $ aka -e 'emacs -nw' I passed in the `-nw` option to emacs so that it doesn't open a graphical window. Because aka recognized emacs, the actual command launched was `emacs -nw +9:14 /tmp/aka_4e1lydcf.py`. ''' HELP = { 'python_expr': """ A python expression that will be evaluated for each filename. The old name of the file will be stored in the variable `fn`. `re` (the python regex module), and `rules` (from the contex package) will also be in scope. It should evaluate to the new filename, or to a unary function taking `fn` as an argument that evalues to the new filename (this makes it more practical to use `rules`).""", 'test_expr': """ Works like -p but will not change any files, it will only show what changes would take place; it will also report conflicts""", 'editor': """ This will open a new temporary file in the editor of your choice where you can create a renaming function, this is handy when you want to rename files in complicated ways. This is basically how the editor will be launched: subprocess.call(shlex.split(EDITOR) + ['/tmp/file.py']). If aka recognizes the editor it will try to inject line:column information into the command as well. {}""".format( "Windows note: `shlex.split` isn't used on Windows." if sys.platform == "win32" else ""), 'dir': "Directories in which to rename files. Defaults to the current working directory.", 'inject': "don't inject arguments to the editor; used with -e.", 'prompt': "Rename files without first consulting the user. Useful if you want to use aka in a script.", 'rollback': """ Automatically roll back changes in the event of an emergency. The exit code will still be 1 if that happens. Could be useful in a script as the --yes option has no effect on the emergency mode.""", 'copy': "Copy files instead of renaming them.", 'file': """ If used in conjunction with `-e` it will open that file instead of creating a new temporary file. If used alone it will work exactly the same, it just won't open an editor first. The file must have a `rename` function taking two arguments `fn` and `dirname`. The file doesn't necessarily need to exist, editors like emacs will create it when you save the changes."""} EDITOR_TEXT = '''import re from os.path import join from contex import rules {} def rename(fn, dirname): return fn """ The function above should return the new filename. If it returns None (or any other falsy falue), then that particular file will be ignored (i.e. not renamed). The arguments `fn` and `dirname` are of type `str`. `dirname` is the absolute path to the directory in which `fn` is located. This code will be executed in Python 3. If you're running `python-mode` in emacs, then you can test out your code in the interpreter by running `C-c C-c`, then switch to the new buffer with `C-x b`. """ ''' def args_for_editor(editor): """ Returns a tuple of cmd line args to `editor`. The tuple returned is of the form (, ) where are the args needed to place the cursor at the right place in the file, while are unrelated arguments which are deemed necessary. It is specifically needed for passing -f to `gvim` so that it stays in the foreground thus blocking this process. """ matches = lambda reg: re.match(r"({})[\d.]*(\.exe)?".format(reg), editor) return ((["+{line}:{col}"], []) if matches("emacs|gedit") else (["+{line},{col}"], []) if matches("nano") else (["+normal {line}G{col}|"], []) if matches("vim?") else (["+normal {line}G{col}|"], ["-f"]) if matches("gvim") else (["+{line}"], []) if matches("(xe|j)macs|zile|r?joe|jstar|jpico") else (["--jump={line}"], []) if matches("leafpad") else (["-l {line}", "-c {col}"], []) if matches("kate") else (["-n{line}", "-c{col}"], []) if matches("notepad\+\+") else ([], [])) def eval_module(text, VARS=None): if VARS is None: VARS = {'re': re, 'contex': contex, 'rules': contex.rules} eval(compile(text, '', 'exec'), VARS) return VARS def rename_with_code(text, args): module = eval_module(text, {}) if not 'rename' in module: print("You need to make a `rename` function in the file.") exit() else: exit(rename_files(args.dir, module['rename'], args.prompt, args.rollback, args.copy, dirname=True)) make_rename_func = """ EXPR = compile({!r}, '', 'eval') def rename_func(fn): result = eval(EXPR) return result(fn) if callable(result) else result""" def rename_files(dirs, machine, prompt, emergency, copy, dirname=False): " Renames/Copies files in all `dirs`, returning True if any of them fails " method = aka.copy if copy else aka.rename failures = False for directory in dirs: print("\n -- {} FILES IN {} --\n".format("COPYING" if copy else "RENAMING", directory)) result = method(machine, directory, prompt, emergency, dirname) failures = failures or not result return failures def content(fname): with open(fname) as fh: return fh.read() def rename_line_number(text): index = text.find("def rename(") if index == -1: return 0 else: return text[:index].count('\n') + 1 if __name__ == '__main__': parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="Renames files with python code; it does so in two passes so as to avoid collisions.\nYou must provide either `-p', `-t' or `-e'. The exit code for aka is 1 if any failures\noccur.", epilog=EXAMPLES) parser.add_argument('-p', help=HELP['python_expr'], dest='python_expr') parser.add_argument('-t', help=HELP['test_expr'], dest='test_expr') parser.add_argument('-e', help=HELP['editor'], dest='editor') parser.add_argument('-f', help=HELP['file'], dest='file') parser.add_argument('dir', help=HELP['dir'], nargs='*', default=['.']) parser.add_argument('--dont-inject', help=HELP['inject'], dest='inject', action='store_false') parser.add_argument('-y', '--yes', help=HELP['prompt'], dest='prompt', action='store_false') parser.add_argument('-r', '--rollback', help=HELP['rollback'], dest='rollback', action='store_const', const="rollback", default=None) parser.add_argument('-c', '--copy', help=HELP['copy'], dest='copy', action='store_true') args = parser.parse_args() if args.test_expr is not None: vars = eval_module(make_rename_func.format(args.test_expr)) for directory in args.dir: class_ = aka.actions.CopyActions if args.copy else aka.actions.RenameActions actions = class_(vars['rename_func'], directory, "/dummy/value") if not actions.report_conflicts(): actions.show_actions() elif ((args.python_expr, args.editor, args.file) == (None, None, None) or args.python_expr and (args.editor or args.file)): parser.print_help() exit() elif args.python_expr: vars = eval_module(make_rename_func.format(args.python_expr)) exit(rename_files(args.dir, vars['rename_func'], args.prompt, args.rollback, args.copy)) elif args.editor: base_command = [args.editor] if sys.platform == "win32" else shlex.split(args.editor) line_col_args, extra_args = args_for_editor(os.path.basename(base_command[0]).lower()) if args.file: filename = args.file if args.inject and os.path.exists(filename): # Hm.. It exists. Lets help the user. linum = rename_line_number(content(filename)) if linum: line_col_args = [arg.format(line=linum, col=0) for arg in line_col_args] else: line_col_args = [] else: line_col_args = [] else: fh = tempfile.NamedTemporaryFile(mode='w', suffix='.py', prefix='aka_', delete=False) filename = fh.name custom_text = EDITOR_TEXT.format( '# Directories in which to perform changes:\n' + '\n'.join('# ' + os.path.abspath(d) for d in args.dir)) fh.write(custom_text) fh.close() line = rename_line_number(custom_text) + 1 col = 14 # I've counted the columns and lines. Trust me. line_col_args = [arg.format(line=line, col=col) for arg in line_col_args] if args.inject: full_command = base_command + line_col_args + extra_args + [filename] else: full_command = base_command + [filename] if sys.platform == "win32": print('running >', full_command) else: print('running $', ' '.join(map(shlex.quote, full_command))) subprocess.call(full_command) # Should block this process result = content(args.file or filename) if not args.file: os.remove(filename) if result == custom_text: print("You didn't change the file; aborting.") exit() if not aka.utils.yes_no_prompt("Aka: Proceed?"): print("As you wish.") exit() rename_with_code(result, args) elif args.file: rename_with_code(content(args.file), args) PKc)Gd= ^!aka-1.0.dist-info/DESCRIPTION.rstAka - Rename files in complicated ways easily ======================================= Abstract --------- This package provides a command line utility called ``aka`` for swiftly renaming (or copying) files using Python code. This makes it easy to rename files even when the changes you are making are quite complicated. It always renames files in two passes to avoid collisions; it tries to detect miscellaneous errors in advance; and if errors occurs underways it will put you in an emergency mode to resolve the problem or roll back changes. It also provides the functions ``aka.rename`` and ``aka.copy``, which is the underlying interface. The problem being solved ------------------------ Lets say you have a directory with the files ``File0``, ``File1``, and ``File2``. Then some people comes along and complains (rightly or wrongly) that the numbering starts at zero. So you decide to write a program to rename all those files, but a problem arises. You cannot do it in any order you like, you have to start with ``File2 -> File3`` in order to avoid conflicts. It'd be nice to just write a function that knows how to change the names of individual files and let another program sort out the rest. This is what ``aka.rename`` is about: .. code-block:: python >>> from contex import rules >>> def machine(fn): return rules(r'File(\d+)', {1: lambda digit: int(digit) + 1}).apply(fn) >>> aka.rename(machine) Actions to be taken (simplified; doesn't show the temporary stage): /home/uglemat/Documents/File0 -> /home/uglemat/Documents/File1 /home/uglemat/Documents/File1 -> /home/uglemat/Documents/File2 /home/uglemat/Documents/File2 -> /home/uglemat/Documents/File3 Target directories: /home/uglemat/Documents The files will be renamed as shown above (in two passes though, in order to avoid collisions). This program searched for name conflicts in all target directories and did not find any. If errors do pop up, you'll be taken to an emergency mode where you can roll back changes. Continue? [N/y]: y Renaming /home/uglemat/Documents/File0 -> /tmp/aka_maok91r8/File0 Renaming /home/uglemat/Documents/File1 -> /tmp/aka_maok91r8/File1 Renaming /home/uglemat/Documents/File2 -> /tmp/aka_maok91r8/File2 Renaming /tmp/aka_maok91r8/File0 -> /home/uglemat/Documents/File1 Renaming /tmp/aka_maok91r8/File1 -> /home/uglemat/Documents/File2 Renaming /tmp/aka_maok91r8/File2 -> /home/uglemat/Documents/File3 True I used `contex.rules `_ to manipulate the string, but you can do whatever you like inside ``machine``, you just need to return the new name of the file. By default it renames files in the current working directory, but that can be changed with the ``location`` argument to ``aka.rename``. ``aka.copy`` is basically the same, it just copies files instead. Read the docstrings of those functions to learn the details. Command line utility -------------------- That's all fine and dandy, but when you just have some files and you need to rename them, you want to do it with a command line utility. This is the basics: .. code-block:: bash $ aka --help Useful information ... $ aka -p 'fn+".jpg"' That will add a ".jpg" suffix to all files in the working directory. But lets do what we did above with ``aka.rename``: .. code-block:: bash $ aka -p 'rules(r"File(\d+)", {1: lambda digit: int(digit) + 1})' The expression after ``-p`` doesn't need to be a new filename, it can also be a unary callable (like ``machine`` above) that returns the new filename. That is why the example above works; ``contex.rules`` returns a callable. More complicated renaming schemes --------------------------------- That's great, but what if it's not a simple one-liner? Then you need to create a new file, write some python code, launch the python interpreter, import the stuff you need... It's cumbersome, which is why ``aka`` can help with that: .. code-block:: bash $ aka -e emacs This will launch emacs and take you to a temporary file which looks kind of like this: .. code-block:: python import re from os.path import join from contex import rules # Directories in which to perform changes: # /home/uglemat/Documents def rename(fn, dirname): return fn Your job is to complete ``rename``, and when you exit the editor it will do the job (after asking you if you want to continue). Lets do something more advanced, say you have lots of files in ``~/Documents/files`` of the format ``File`` and you want to split them into the folders ``odd`` and ``even``, like this: .. code-block:: bash ~/Documents/files $ for i in {0..20}; do touch "File$i"; done ~/Documents/files $ ls File0 File1 File10 File11 File12 File13 File14 File15 File16 File17 File18 File19 File2 File20 File3 File4 File5 File6 File7 File8 File9 ~/Documents/files $ mkdir odd even There is a slight problem in that you can't rename ``odd`` and ``even``, but they are in the same directory. You just got to make sure that the rename function returns a falsy value for those filenames (btw, aka treats directories like files and will rename them too). Lets go to the editor with ``aka -e 'emacs -nw'`` and write this: .. code-block:: python import re from os.path import join from contex import rules # Directories in which to perform changes: # /home/uglemat/Documents/files def rename(fn, dirname): match = re.search(r'\d+', fn) if match: digit = int(match.group(0)) return join('even' if even(digit) else 'odd', fn) def even(d): return (d % 2) == 0 The directories ``odd`` and ``even`` doesn't match, so ``rename`` returns ``None`` for those names and thus they are ignored, and the code above works as expected: .. code-block:: bash ~/Documents/files $ aka -e 'emacs -nw' running $ emacs -nw +9:14 /tmp/aka_3uvuyn8c.py Aka: Proceed? [Y/n]: y -- RENAMING FILES IN . -- Actions to be taken (simplified; doesn't show the temporary stage): /home/uglemat/Documents/files/File3 -> /home/uglemat/Documents/files/odd/File3 /home/uglemat/Documents/files/File18 -> /home/uglemat/Documents/files/even/File18 /home/uglemat/Documents/files/File13 -> /home/uglemat/Documents/files/odd/File13 ... Target directories: /home/uglemat/Documents/files/odd /home/uglemat/Documents/files/even The files will be renamed as shown above (in two passes though, in order to avoid collisions). This program searched for name conflicts in all target directories and did not find any. If errors do pop up, you'll be taken to an emergency mode where you can roll back changes. Continue? [N/y]: y Renaming /home/uglemat/Documents/files/File3 -> /tmp/aka_st72r5jp/File3 Renaming /home/uglemat/Documents/files/File18 -> /tmp/aka_st72r5jp/File18 Renaming /home/uglemat/Documents/files/File13 -> /tmp/aka_st72r5jp/File13 ... Renaming /tmp/aka_st72r5jp/File3 -> /home/uglemat/Documents/files/odd/File3 Renaming /tmp/aka_st72r5jp/File18 -> /home/uglemat/Documents/files/even/File18 Renaming /tmp/aka_st72r5jp/File13 -> /home/uglemat/Documents/files/odd/File13 ~/Documents/files $ ls * even: File0 File10 File12 File14 File16 File18 File2 File20 File4 File6 File8 odd: File1 File11 File13 File15 File17 File19 File3 File5 File7 File9 Installing ---------- ``aka`` works only in Python 3. Install with ``$ pip3 install aka``. You might want to replace ``pip3`` with ``pip``, depending on how your system is configured. Developing ---------- Aka has some test. Run ``$ nosetests`` or ``$ python3 setup.py test`` to run the tests. The code is hosted at https://notabug.org/Uglemat/aka You can install in development mode with ``$ pip3 install -e .``, then your changes to aka will take effect immediately. License ------- The library is licensed under the GNU General Public License 3 or later. This README file is public domain. PKc)GO\\aka-1.0.dist-info/metadata.json{"classifiers": ["Environment :: Console", "Intended Audience :: System Administrators", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Topic :: System :: Systems Administration", "Topic :: Utilities", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Programming Language :: Python :: 3"], "extensions": {"python.details": {"contacts": [{"email": "uglemat@gmail.com", "name": "Mattias Ugelvik", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://notabug.org/Uglemat/aka"}}}, "extras": [], "generator": "bdist_wheel (0.24.0)", "license": "GPL3+", "metadata_version": "2.0", "name": "aka", "run_requires": [{"requires": ["contex"]}], "summary": "Rename/copy files using Python code", "test_requires": [{"requires": ["nose"]}], "version": "1.0"}PKb)GCozaka-1.0.dist-info/top_level.txtaka PKc)G[C\\aka-1.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.24.0) Root-Is-Purelib: true Tag: py3-none-any PKc)GUiQQ"Q"aka-1.0.dist-info/METADATAMetadata-Version: 2.0 Name: aka Version: 1.0 Summary: Rename/copy files using Python code Home-page: https://notabug.org/Uglemat/aka Author: Mattias Ugelvik Author-email: uglemat@gmail.com License: GPL3+ Platform: UNKNOWN Classifier: Environment :: Console Classifier: Intended Audience :: System Administrators Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Programming Language :: Python :: 3 Requires-Dist: contex Aka - Rename files in complicated ways easily ======================================= Abstract --------- This package provides a command line utility called ``aka`` for swiftly renaming (or copying) files using Python code. This makes it easy to rename files even when the changes you are making are quite complicated. It always renames files in two passes to avoid collisions; it tries to detect miscellaneous errors in advance; and if errors occurs underways it will put you in an emergency mode to resolve the problem or roll back changes. It also provides the functions ``aka.rename`` and ``aka.copy``, which is the underlying interface. The problem being solved ------------------------ Lets say you have a directory with the files ``File0``, ``File1``, and ``File2``. Then some people comes along and complains (rightly or wrongly) that the numbering starts at zero. So you decide to write a program to rename all those files, but a problem arises. You cannot do it in any order you like, you have to start with ``File2 -> File3`` in order to avoid conflicts. It'd be nice to just write a function that knows how to change the names of individual files and let another program sort out the rest. This is what ``aka.rename`` is about: .. code-block:: python >>> from contex import rules >>> def machine(fn): return rules(r'File(\d+)', {1: lambda digit: int(digit) + 1}).apply(fn) >>> aka.rename(machine) Actions to be taken (simplified; doesn't show the temporary stage): /home/uglemat/Documents/File0 -> /home/uglemat/Documents/File1 /home/uglemat/Documents/File1 -> /home/uglemat/Documents/File2 /home/uglemat/Documents/File2 -> /home/uglemat/Documents/File3 Target directories: /home/uglemat/Documents The files will be renamed as shown above (in two passes though, in order to avoid collisions). This program searched for name conflicts in all target directories and did not find any. If errors do pop up, you'll be taken to an emergency mode where you can roll back changes. Continue? [N/y]: y Renaming /home/uglemat/Documents/File0 -> /tmp/aka_maok91r8/File0 Renaming /home/uglemat/Documents/File1 -> /tmp/aka_maok91r8/File1 Renaming /home/uglemat/Documents/File2 -> /tmp/aka_maok91r8/File2 Renaming /tmp/aka_maok91r8/File0 -> /home/uglemat/Documents/File1 Renaming /tmp/aka_maok91r8/File1 -> /home/uglemat/Documents/File2 Renaming /tmp/aka_maok91r8/File2 -> /home/uglemat/Documents/File3 True I used `contex.rules `_ to manipulate the string, but you can do whatever you like inside ``machine``, you just need to return the new name of the file. By default it renames files in the current working directory, but that can be changed with the ``location`` argument to ``aka.rename``. ``aka.copy`` is basically the same, it just copies files instead. Read the docstrings of those functions to learn the details. Command line utility -------------------- That's all fine and dandy, but when you just have some files and you need to rename them, you want to do it with a command line utility. This is the basics: .. code-block:: bash $ aka --help Useful information ... $ aka -p 'fn+".jpg"' That will add a ".jpg" suffix to all files in the working directory. But lets do what we did above with ``aka.rename``: .. code-block:: bash $ aka -p 'rules(r"File(\d+)", {1: lambda digit: int(digit) + 1})' The expression after ``-p`` doesn't need to be a new filename, it can also be a unary callable (like ``machine`` above) that returns the new filename. That is why the example above works; ``contex.rules`` returns a callable. More complicated renaming schemes --------------------------------- That's great, but what if it's not a simple one-liner? Then you need to create a new file, write some python code, launch the python interpreter, import the stuff you need... It's cumbersome, which is why ``aka`` can help with that: .. code-block:: bash $ aka -e emacs This will launch emacs and take you to a temporary file which looks kind of like this: .. code-block:: python import re from os.path import join from contex import rules # Directories in which to perform changes: # /home/uglemat/Documents def rename(fn, dirname): return fn Your job is to complete ``rename``, and when you exit the editor it will do the job (after asking you if you want to continue). Lets do something more advanced, say you have lots of files in ``~/Documents/files`` of the format ``File`` and you want to split them into the folders ``odd`` and ``even``, like this: .. code-block:: bash ~/Documents/files $ for i in {0..20}; do touch "File$i"; done ~/Documents/files $ ls File0 File1 File10 File11 File12 File13 File14 File15 File16 File17 File18 File19 File2 File20 File3 File4 File5 File6 File7 File8 File9 ~/Documents/files $ mkdir odd even There is a slight problem in that you can't rename ``odd`` and ``even``, but they are in the same directory. You just got to make sure that the rename function returns a falsy value for those filenames (btw, aka treats directories like files and will rename them too). Lets go to the editor with ``aka -e 'emacs -nw'`` and write this: .. code-block:: python import re from os.path import join from contex import rules # Directories in which to perform changes: # /home/uglemat/Documents/files def rename(fn, dirname): match = re.search(r'\d+', fn) if match: digit = int(match.group(0)) return join('even' if even(digit) else 'odd', fn) def even(d): return (d % 2) == 0 The directories ``odd`` and ``even`` doesn't match, so ``rename`` returns ``None`` for those names and thus they are ignored, and the code above works as expected: .. code-block:: bash ~/Documents/files $ aka -e 'emacs -nw' running $ emacs -nw +9:14 /tmp/aka_3uvuyn8c.py Aka: Proceed? [Y/n]: y -- RENAMING FILES IN . -- Actions to be taken (simplified; doesn't show the temporary stage): /home/uglemat/Documents/files/File3 -> /home/uglemat/Documents/files/odd/File3 /home/uglemat/Documents/files/File18 -> /home/uglemat/Documents/files/even/File18 /home/uglemat/Documents/files/File13 -> /home/uglemat/Documents/files/odd/File13 ... Target directories: /home/uglemat/Documents/files/odd /home/uglemat/Documents/files/even The files will be renamed as shown above (in two passes though, in order to avoid collisions). This program searched for name conflicts in all target directories and did not find any. If errors do pop up, you'll be taken to an emergency mode where you can roll back changes. Continue? [N/y]: y Renaming /home/uglemat/Documents/files/File3 -> /tmp/aka_st72r5jp/File3 Renaming /home/uglemat/Documents/files/File18 -> /tmp/aka_st72r5jp/File18 Renaming /home/uglemat/Documents/files/File13 -> /tmp/aka_st72r5jp/File13 ... Renaming /tmp/aka_st72r5jp/File3 -> /home/uglemat/Documents/files/odd/File3 Renaming /tmp/aka_st72r5jp/File18 -> /home/uglemat/Documents/files/even/File18 Renaming /tmp/aka_st72r5jp/File13 -> /home/uglemat/Documents/files/odd/File13 ~/Documents/files $ ls * even: File0 File10 File12 File14 File16 File18 File2 File20 File4 File6 File8 odd: File1 File11 File13 File15 File17 File19 File3 File5 File7 File9 Installing ---------- ``aka`` works only in Python 3. Install with ``$ pip3 install aka``. You might want to replace ``pip3`` with ``pip``, depending on how your system is configured. Developing ---------- Aka has some test. Run ``$ nosetests`` or ``$ python3 setup.py test`` to run the tests. The code is hosted at https://notabug.org/Uglemat/aka You can install in development mode with ``$ pip3 install -e .``, then your changes to aka will take effect immediately. License ------- The library is licensed under the GNU General Public License 3 or later. This README file is public domain. PKc)Gşgaka-1.0.dist-info/RECORDaka/__init__.py,sha256=wXuFJ9d0Xf3F_p0VWF3UhK_tPIuHTpW0LTzVtEqzPQE,4258 aka/actions.py,sha256=M5gyNMNadBMxUzKanajvrwgTwUSdUBvhVh6g2g8QR80,9843 aka/utils.py,sha256=q5f6Jrly2_l-HRxZFrSVCyM6MGt8nkvk-CzUNUyWO7Y,2217 aka-1.0.dist-info/RECORD,, aka-1.0.dist-info/METADATA,sha256=AkIm05kLEY4vYdnRu1se59Qiv5XguT-2HDKqvqgsDuw,8785 aka-1.0.dist-info/WHEEL,sha256=-aSo8rHuuPDEFzkcqqQ55pDyCjy25bYMLxSiHWKAOTc,92 aka-1.0.dist-info/metadata.json,sha256=J2FJ-OcvxCF_Ng3jnUEv3gvwz3ma5ku-ZgUh-beCdEk,860 aka-1.0.dist-info/top_level.txt,sha256=CTbRd6bIExjiechpaZxTLZ2_mgwG_mVfAeQFN9eZww0,4 aka-1.0.dist-info/DESCRIPTION.rst,sha256=FN439tfkM8z8z0N4FmRV_CTh_MLFn9GrGwMljWA3YCc,8135 aka-1.0.data/scripts/aka,sha256=n513oB6PSrhn6XTkL0IO9j7k9yaz-vR_LJPXvriF0Bg,11425 PK%G3Z@Yaka/__init__.pyPK&GIKs&s&aka/actions.pyPKR&G$Ru n7aka/utils.pyPKۖ)G9,,A@aka-1.0.data/scripts/akaPKc)Gd= ^!maka-1.0.dist-info/DESCRIPTION.rstPKc)GO\\aka-1.0.dist-info/metadata.jsonPKb)GCozaka-1.0.dist-info/top_level.txtPKc)G[C\\aka-1.0.dist-info/WHEELPKc)GUiQQ"Q"aka-1.0.dist-info/METADATAPKc)Gşgaka-1.0.dist-info/RECORDPK :