From 365cb466634f2ce63028c26ffd95468f7cf712a4 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 27 Sep 2013 13:37:05 -0400 Subject: Initial commit --- .gitignore | 2 + lnmod.py | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .gitignore create mode 100755 lnmod.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c4b3d58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.*project +test/ diff --git a/lnmod.py b/lnmod.py new file mode 100755 index 0000000..a36593c --- /dev/null +++ b/lnmod.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +#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 os +import argparse +import signal +from collections import namedtuple + + +#Define a namedtuple to cleanly handle symbolic link data. +#Because we are performing potentially dangerous operations on the filesystem +#it is necessary to store data in a tuple. E.G. Write once, examine, execute. +#... Can't afford to screw up. +Link = namedtuple('Link', ['path', 'destination']) + + +def readlinkabs(l): + """Return an absolute path for the destination + of a symlink + """ + if not os.path.islink(l): + return '' + p = os.readlink(l) + if os.path.isabs(p): + return p + return os.path.abspath(os.path.join(os.path.dirname(l), p)) + +def generate_link_map(path): + """Walk through "path" directory structure and record all symbolic links, + and their destinations. + + return: list of Link namedtuples + """ + original_paths = [] + link_paths = [] + links = [] + + for root, dirnames, filenames in path: + # For each directory + for dirname in dirnames: + dir_path = os.path.join(root, dirname) + if readlinkabs(dir_path): + original_paths.append(dir_path) + link_paths.append(readlinkabs(dir_path)) + + #For each file + for filename in filenames: + file_path = os.path.join(root, filename) + if readlinkabs(file_path): + # The way os.walk nests filenames; ignore replicated entities + if file_path in original_paths or file_path in link_paths: + continue + #Store symbolic link paths + original_paths.append(file_path) + link_paths.append(readlinkabs(file_path)) + #Generate a list of Links + for original_path, link_path in zip(original_paths, link_paths): + links.append(Link(original_path, link_path)) + return links + +def get_link_map(link_map): + """return: yield Link namedtuples + """ + for link in link_map: + yield link + +def generate_replacement_link_map(link_map, search, replace): + """Find and replace occurrence of search parameter in link. + return: list of Link namedtuples + """ + links = [] + for link in link_map: + if search in link.destination: + linkr = link.destination.replace(search, replace) + if linkr: + #This should never happen... but if it does, we're ready. + if link.path == linkr: + print("Ignoring (dangerous) duplicated path: {}".format(link.path)) + continue + links.append(Link(link.path, linkr)) + return links + +def replace_links(link_map): + """Perform physical link replacement using link_map data. + In order to update the symbolic links in the filesystem they + must be removed and regenerated. + return: None + """ + for link in link_map: + if not os.path.exists(link.path): + print("WARNING :: Destination {0} does not exist. May generate a dead link.".format(link.destination)) + + print("Re-linking: {0} -> {1}".format(link.path, link.destination)) + os.unlink(link.path) + os.symlink(link.destination, link.path) + +def user_choice(): + """Would you like to play a game? + """ + YES = ['YES', 'Y'] + choice = raw_input().upper() + if choice not in YES: + return False + return True + +def trap_sigint(sig, frame): + """Perform any cleanup tasks (most likely none) + """ + print("Caught signal {0}. Exiting.".format(sig)) + exit(0) + +if __name__ == "__main__": + signal.signal(signal.SIGINT, trap_sigint) + parser = argparse.ArgumentParser(description='Replace symbolic links') + parser.add_argument('--force', action="store_true", help='Do not ask for confirmation') + parser.add_argument('search', type=str, action="store", help='original path pattern') + parser.add_argument('replace', type=str, action="store", help='replacement string in path pattern') + parser.add_argument('path', type=str, action="store", help='path to perform search') + args = parser.parse_args() + + if args.search == args.replace or \ + args.replace == args.search: + print("Search and replace patterns cannot be the same.") + exit(1) + + lmap = generate_link_map(os.walk(os.path.abspath(args.path), followlinks=False)) + if not lmap: + print("No symbolic links detected under {0}".format(args.path)) + exit(0) + + print("### LINKS ####") + for link in get_link_map(lmap): + print("Link: {0} -> {1}".format(link.path, link.destination)) + + rmap = generate_replacement_link_map(lmap, args.search, args.replace) + if not rmap: + print("Nothing to replace.".format(args.search, args.path)) + exit(0) + + print("") + print("### PREVIEW ####") + for link in get_link_map(rmap): + print("Replacement: {0} -> {1}".format(link.path, link.destination)) + + link_total = len(lmap) + match_total = len(rmap) + print("") + print("{0} symbolic links total, {1} pattern matches".format(link_total, match_total)) + print("") + if args.force: + replace_links(rmap) + else: + print("Replace symbolic link(s)? [y/N]"), + if user_choice(): + print("") + replace_links(rmap) + else: + print("Abort.") + exit(254) + exit(0) \ No newline at end of file -- cgit