#!/usr/bin/python3 # # unln.py, Copyright © 2013 Matteo Cypriani # ######################################################################## # This program is licensed under the terms of the Expat 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. ######################################################################## # # This script separates file names given as arguments from their respective # inodes by doing the equivalent of (cp -p file tmp && mv tmp file). # # TODO: # - Handle exceptions (permission denied on copy, etc.) # - Option verbose # - Option to follow symlinks # - Summarise skipped regular files with more than one link import sys import os import tempfile import shutil def verbose(*message, **args): """Prints the message on the standard output if verbose mode is on. This function is a simple wrapper around print(), and you can use any keyword argument you would use with print(). """ if options["verbose"]: print(*message, **args) def warn(*message, prefix="Warning:", **args): """Prints the message on the error output, prepended by 'prefix'. This function is a simple wrapper around print(), and you can use any keyword argument you would use with print(). """ print(prefix, *message, file=sys.stderr, **args) def error(error_code, *message, **args): """Prints the message on the error output and exits. This function is a simple wrapper around print(), and you can use any keyword argument you would use with print(). """ print(*message, file=sys.stderr, **args) sys.exit(error_code) def quote(string): """ Quotes a string. """ return "``{}´´".format(string) # Default options options = { "verbose": True } # Name of this program progname = os.path.basename(sys.argv[0]) # Short usage string usage_string = "Usage:\n\t{} [file2 ...]\n".format(progname) # Error codes ERR_BAD_USAGE = 1 # Check command-line arguments if len(sys.argv) < 2: error(ERR_BAD_USAGE, usage_string) # Main loop for filename in sys.argv[1:]: # Exists? if not os.path.exists(filename): warn(quote(filename), "does not exist, ignoring.") continue # Is a symbolic link? if os.path.islink(filename): warn(quote(filename), "is a symbolic link, ignoring.") continue # Is a regular file? if not os.path.isfile(filename): warn(quote(filename), "is not a regular file, ignoring.") continue # Number of links? nlinks = os.stat(filename).st_nlink if nlinks == 1: verbose(quote(filename), "has only one link, ignoring.") continue verbose(quote(filename), "has", nlinks, "hard links, proceeding", end="... ") # Reserve a temporary file name dirname = os.path.dirname(filename) handle, tmpfilename = tempfile.mkstemp( prefix="{}-".format(filename), suffix="-{}.tmp".format(progname), dir=dirname) os.close(handle) # we just need a temporary file name # Copy the original file to the temporary file verbose("Copying to temporary file", quote(tmpfilename), end="... ") shutil.copy2(filename, tmpfilename) # cp -p # Rename the temporary file with the original file name verbose("Moving back {} to {}...".format( quote(tmpfilename), quote(filename))) os.rename(tmpfilename, filename)